Add portal web simulator, SPI bridges, and Pico firmware updates.
Bring the five-panel hex portal online with a browser 3D/schematic preview, Pi SPI backends, and renamed multi-panel Pico UDP firmware. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
169
web/js/components/portal-schematic.js
Normal file
169
web/js/components/portal-schematic.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
SCHEMATIC_FLOOR_LINE,
|
||||
SCHEMATIC_HEX_PATH,
|
||||
SCHEMATIC_PANEL_LAYOUT,
|
||||
} from "../portal-layout.js";
|
||||
import "./portal-panel.js";
|
||||
|
||||
const SCHEMATIC_STYLES = `
|
||||
:host {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
background: radial-gradient(ellipse at 50% 45%, #141820 0%, #0a0c12 70%);
|
||||
}
|
||||
:host([visible]) {
|
||||
display: flex;
|
||||
}
|
||||
.hex-stage {
|
||||
position: relative;
|
||||
width: min(88vw, 420px);
|
||||
aspect-ratio: 100 / 118;
|
||||
}
|
||||
.hex-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hex-fill {
|
||||
fill: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.hex-stroke {
|
||||
fill: none;
|
||||
stroke: #3a4458;
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
.hex-floor {
|
||||
fill: none;
|
||||
stroke: #2a3044;
|
||||
stroke-width: 0.8;
|
||||
stroke-dasharray: 3 3;
|
||||
}
|
||||
.floor-label {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 91%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: #5a6278;
|
||||
font-family: system-ui, sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ground {
|
||||
position: absolute;
|
||||
left: 5%;
|
||||
right: 5%;
|
||||
bottom: -2%;
|
||||
height: 2px;
|
||||
background: #3a4050;
|
||||
}
|
||||
.slot {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
}
|
||||
.slot portal-panel {
|
||||
display: block;
|
||||
transform-origin: center center;
|
||||
box-shadow: 0 0 12px rgba(255, 60, 40, 0.15);
|
||||
}
|
||||
`;
|
||||
|
||||
export class PortalSchematic extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {Map<number, import('./portal-panel.js').PortalPanel>} */
|
||||
this._panels = new Map();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot) return;
|
||||
const root = this.attachShadow({ mode: "open" });
|
||||
root.innerHTML = `
|
||||
<style>${SCHEMATIC_STYLES}</style>
|
||||
<div class="hex-stage">
|
||||
<svg class="hex-svg" viewBox="0 0 100 108" preserveAspectRatio="xMidYMid meet">
|
||||
<path class="hex-fill" d="${SCHEMATIC_HEX_PATH}" />
|
||||
<path class="hex-stroke" d="${SCHEMATIC_HEX_PATH}" />
|
||||
<line class="hex-floor"
|
||||
x1="${SCHEMATIC_FLOOR_LINE.x1}" y1="${SCHEMATIC_FLOOR_LINE.y1}"
|
||||
x2="${SCHEMATIC_FLOOR_LINE.x2}" y2="${SCHEMATIC_FLOOR_LINE.y2}" />
|
||||
</svg>
|
||||
<div class="floor-label">floor · walk through</div>
|
||||
<div class="ground"></div>
|
||||
<div class="slots"></div>
|
||||
</div>
|
||||
`;
|
||||
this._slotsEl = root.querySelector(".slots");
|
||||
}
|
||||
|
||||
_ensureSlot(position) {
|
||||
let slot = this._slotsEl.querySelector(`[data-position="${position}"]`);
|
||||
if (slot) return slot;
|
||||
|
||||
const layout = SCHEMATIC_PANEL_LAYOUT[position];
|
||||
if (!layout) return null;
|
||||
|
||||
slot = document.createElement("div");
|
||||
slot.className = "slot";
|
||||
slot.dataset.position = position;
|
||||
slot.style.left = `${layout.x}%`;
|
||||
slot.style.top = `${layout.y}%`;
|
||||
slot.style.maxWidth = layout.maxW;
|
||||
this._slotsEl.appendChild(slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
/** @param {import('../api.js').PanelConfig[]} panels */
|
||||
setPanels(panels) {
|
||||
if (!this.shadowRoot) this.connectedCallback();
|
||||
this._panels.clear();
|
||||
this._slotsEl.querySelectorAll(".slot").forEach((s) => {
|
||||
s.innerHTML = "";
|
||||
});
|
||||
|
||||
for (const panel of panels) {
|
||||
const slot = this._ensureSlot(panel.position);
|
||||
if (!slot) continue;
|
||||
|
||||
const layout = SCHEMATIC_PANEL_LAYOUT[panel.position];
|
||||
const el = document.createElement("portal-panel");
|
||||
el.setAttribute("index", String(panel.index));
|
||||
el.setAttribute("position", panel.position);
|
||||
el.setAttribute("width", String(panel.width));
|
||||
el.setAttribute("height", String(panel.height));
|
||||
el.style.transform = `rotate(${layout.rot}deg)`;
|
||||
slot.appendChild(el);
|
||||
this._panels.set(panel.index, el);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
* @param {number[] | Uint8Array} rgb
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {boolean} identify
|
||||
*/
|
||||
updatePanel(index, rgb, width, height, identify) {
|
||||
const panel = this._panels.get(index);
|
||||
if (!panel) return;
|
||||
if (identify) panel.showIdentify();
|
||||
else panel.setFrame(rgb, width, height);
|
||||
}
|
||||
|
||||
set visible(on) {
|
||||
if (on) this.setAttribute("visible", "");
|
||||
else this.removeAttribute("visible");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("portal-schematic", PortalSchematic);
|
||||
Reference in New Issue
Block a user