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:
2026-07-30 14:54:51 +12:00
parent d5cab2efdf
commit 5094c7bcee
78 changed files with 62251 additions and 832 deletions

View 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);