Files
portal/web/js/portal-geometry.js
Jimmy 5094c7bcee 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>
2026-07-30 14:54:51 +12:00

169 lines
4.4 KiB
JavaScript

/** Hexagonal portal geometry — matches physical flat-top hex arch. */
/** Scene scale (~2.5 m tall portal). */
export const PORTAL = {
/** Center to flat-edge (apothem) of inner opening. */
apothem: 1.05,
height: 1.85,
frameDepth: 0.14,
frameWall: 0.09,
baseThickness: 0.07,
baseExtend: 0.18,
ledInset: 0.02,
};
const CELL = 0.078;
/**
* Flat-top hex vertex ring in XZ (y = 0).
* @param {number} apothem
* @returns {[number, number][]}
*/
export function hexVerticesXZ(apothem) {
const R = (apothem * 2) / Math.sqrt(3);
const verts = [];
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 6 + (i * Math.PI) / 3;
verts.push([R * Math.cos(angle), R * Math.sin(angle)]);
}
return verts;
}
/**
* @param {[number, number]} a
* @param {[number, number]} b
*/
function edgeMid(a, b) {
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
}
/**
* Inward normal for flat-top hex edge (XZ plane).
* @param {number} edgeIndex 0..5 clockwise from lower-right sloped edge
*/
function edgeInwardNormal(verts, edgeIndex) {
const a = verts[edgeIndex];
const b = verts[(edgeIndex + 1) % 6];
const dx = b[0] - a[0];
const dz = b[1] - a[1];
const len = Math.hypot(dx, dz) || 1;
const nx = dz / len;
const nz = -dx / len;
const mx = (a[0] + b[0]) / 2;
const mz = (a[1] + b[1]) / 2;
if (mx * nx + mz * nz < 0) {
return [-nx, -nz];
}
return [nx, nz];
}
/**
* Wall panel placement on inner hex face.
* Edge mapping (clockwise from bottom-right sloped edge):
* 0 bottom-right → panel 4
* 1 bottom (floor, no LEDs)
* 2 bottom-left → panel 0
* 3 top-left → panel 1
* 4 top (panel 2 is horizontal ceiling, not this edge)
* 5 top-right → panel 3
* @param {string} position
* @param {number} pixelWidth
*/
export function panelPlacement(position, pixelWidth = 45) {
const { apothem, height, frameDepth, ledInset } = PORTAL;
const verts = hexVerticesXZ(apothem);
const h = 9 * CELL;
const w = pixelWidth * CELL;
const wallY = height * 0.42;
const topY = height - frameDepth * 0.5;
/** @type {Record<string, { edge: number, y: number, rotYExtra?: number }>} */
const wallMap = {
"bottom-right": { edge: 0, y: wallY * 0.72 },
"bottom-left": { edge: 2, y: wallY * 0.72 },
"top-left": { edge: 3, y: wallY * 1.38 },
"top-right": { edge: 5, y: wallY * 1.38 },
};
if (position === "top") {
return {
pos: [0, topY, 0],
rot: [-Math.PI / 2, 0, 0],
width: 45 * CELL,
height: h,
};
}
const spec = wallMap[position];
if (!spec) {
return { pos: [0, 0, 0], rot: [0, 0, 0], width: w, height: h };
}
const a = verts[spec.edge];
const b = verts[(spec.edge + 1) % 6];
const mid = edgeMid(a, b);
const [nx, nz] = edgeInwardNormal(verts, spec.edge);
const inset = frameDepth * 0.5 + ledInset;
const px = mid[0] + nx * inset;
const pz = mid[1] + nz * inset;
const rotY = Math.atan2(nx, nz);
return {
pos: [px, spec.y, pz],
rot: [0, rotY, 0],
width: w,
height: h,
};
}
/**
* Build dark frame beam meshes data for each hex edge.
* @returns {Array<{ from: [number,number], to: [number,number], isFloor: boolean }>}
*/
export function hexFrameEdges() {
const outer = hexVerticesXZ(PORTAL.apothem + PORTAL.frameWall);
const inner = hexVerticesXZ(PORTAL.apothem);
const edges = [];
for (let i = 0; i < 6; i++) {
edges.push({
outerA: outer[i],
outerB: outer[(i + 1) % 6],
innerA: inner[i],
innerB: inner[(i + 1) % 6],
isFloor: i === 1,
});
}
return edges;
}
export { CELL };
/** @type {Record<string, string>} */
export const SCHEMATIC_SLOTS = {
"top-left": "slot-tl",
top: "slot-top",
"top-right": "slot-tr",
"bottom-left": "slot-bl",
"bottom-right": "slot-br",
};
/** Front elevation of upright flat-top hex arch (viewBox 0 0 100 108). */
export const SCHEMATIC_HEX_PATH =
"M 20 4 L 80 4 L 96 48 L 80 96 L 20 96 L 4 48 Z";
/** Bottom opening — floor, no LEDs. */
export const SCHEMATIC_FLOOR_LINE = { x1: 20, y1: 96, x2: 80, y2: 96 };
/**
* Panel placement on standing front view (% of stage, rotation deg).
* @type {Record<string, { x: number, y: number, rot: number, maxW: string }>}
*/
export const SCHEMATIC_PANEL_LAYOUT = {
top: { x: 50, y: 7, rot: 0, maxW: "52%" },
"top-left": { x: 15, y: 30, rot: -58, maxW: "34%" },
"top-right": { x: 85, y: 30, rot: 58, maxW: "34%" },
"bottom-left": { x: 15, y: 76, rot: 58, maxW: "32%" },
"bottom-right": { x: 85, y: 76, rot: -58, maxW: "32%" },
};