/** 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} */ 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} */ 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} */ 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%" }, };