(function () { "use strict"; const state = { initialized: false, controlsBound: false, cube: null, hebrewLetters: null, kabbalahPathsByLetterId: new Map(), markerDisplayMode: "both", rotationX: 18, rotationY: -28, selectedNodeType: "wall", showConnectorLines: true, showPrimalPoint: true, selectedConnectorId: null, selectedWallId: null, selectedEdgeId: null }; const CUBE_VERTICES = [ [-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1] ]; const FACE_GEOMETRY = { north: [4, 5, 6, 7], south: [1, 0, 3, 2], east: [5, 1, 2, 6], west: [0, 4, 7, 3], above: [0, 1, 5, 4], below: [7, 6, 2, 3] }; const EDGE_GEOMETRY = [ [0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7] ]; const WALL_ORDER = ["north", "south", "east", "west", "above", "below"]; const EDGE_ORDER = [ "north-east", "south-east", "east-above", "east-below", "north-above", "north-below", "north-west", "south-west", "west-above", "west-below", "south-above", "south-below" ]; const EDGE_GEOMETRY_KEYS = [ "south-above", "south-east", "south-below", "south-west", "north-above", "north-east", "north-below", "north-west", "west-above", "east-above", "east-below", "west-below" ]; const CUBE_VIEW_CENTER = { x: 110, y: 108 }; const LOCAL_DIRECTION_ORDER = ["east", "south", "west", "north"]; const LOCAL_DIRECTION_RANK = { east: 0, south: 1, west: 2, north: 3 }; const LOCAL_DIRECTION_VIEW_MAP = { north: "east", east: "south", south: "west", west: "north" }; const MOTHER_CONNECTORS = [ { id: "above-below", fromWallId: "above", toWallId: "below", hebrewLetterId: "alef", name: "Above ↔ Below" }, { id: "east-west", fromWallId: "east", toWallId: "west", hebrewLetterId: "mem", name: "East ↔ West" }, { id: "south-north", fromWallId: "south", toWallId: "north", hebrewLetterId: "shin", name: "South ↔ North" } ]; const WALL_FRONT_ROTATIONS = { north: { x: 0, y: 0 }, south: { x: 0, y: 180 }, east: { x: 0, y: -90 }, west: { x: 0, y: 90 }, above: { x: -90, y: 0 }, below: { x: 90, y: 0 } }; function getElements() { return { viewContainerEl: document.getElementById("cube-view-container"), rotateLeftEl: document.getElementById("cube-rotate-left"), rotateRightEl: document.getElementById("cube-rotate-right"), rotateUpEl: document.getElementById("cube-rotate-up"), rotateDownEl: document.getElementById("cube-rotate-down"), rotateResetEl: document.getElementById("cube-rotate-reset"), markerModeEl: document.getElementById("cube-marker-mode"), connectorToggleEl: document.getElementById("cube-connector-toggle"), primalToggleEl: document.getElementById("cube-primal-toggle"), rotationReadoutEl: document.getElementById("cube-rotation-readout"), detailNameEl: document.getElementById("cube-detail-name"), detailSubEl: document.getElementById("cube-detail-sub"), detailBodyEl: document.getElementById("cube-detail-body") }; } function normalizeId(value) { return String(value || "").trim().toLowerCase(); } function normalizeLetterKey(value) { const key = normalizeId(value).replace(/[^a-z]/g, ""); const aliases = { aleph: "alef", beth: "bet", zain: "zayin", cheth: "het", chet: "het", daleth: "dalet", kaf: "kaf", kaph: "kaf", teth: "tet", peh: "pe", tzaddi: "tsadi", tzadi: "tsadi", tzade: "tsadi", tsaddi: "tsadi", qoph: "qof", taw: "tav", tau: "tav" }; return aliases[key] || key; } function asRecord(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function getWalls() { const walls = Array.isArray(state.cube?.walls) ? state.cube.walls : []; return walls .slice() .sort((left, right) => WALL_ORDER.indexOf(normalizeId(left?.id)) - WALL_ORDER.indexOf(normalizeId(right?.id))); } function getWallById(wallId) { const target = normalizeId(wallId); return getWalls().find((wall) => normalizeId(wall?.id) === target) || null; } function normalizeEdgeId(value) { return normalizeId(value).replace(/[\s_]+/g, "-"); } function formatEdgeName(edgeId) { return normalizeEdgeId(edgeId) .split("-") .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function formatDirectionName(direction) { const key = normalizeId(direction); return key ? `${key.charAt(0).toUpperCase()}${key.slice(1)}` : ""; } function getEdges() { const configuredEdges = Array.isArray(state.cube?.edges) ? state.cube.edges : []; const byId = new Map( configuredEdges.map((edge) => [normalizeEdgeId(edge?.id), edge]) ); return EDGE_ORDER.map((edgeId) => { const configured = byId.get(edgeId); if (configured) { return configured; } return { id: edgeId, name: formatEdgeName(edgeId), walls: edgeId.split("-") }; }); } function getEdgeById(edgeId) { const target = normalizeEdgeId(edgeId); return getEdges().find((edge) => normalizeEdgeId(edge?.id) === target) || null; } function getEdgeWalls(edge) { const explicitWalls = Array.isArray(edge?.walls) ? edge.walls.map((wallId) => normalizeId(wallId)).filter(Boolean) : []; if (explicitWalls.length >= 2) { return explicitWalls.slice(0, 2); } return normalizeEdgeId(edge?.id) .split("-") .map((wallId) => normalizeId(wallId)) .filter(Boolean) .slice(0, 2); } function getEdgesForWall(wallOrWallId) { const wallId = normalizeId(typeof wallOrWallId === "string" ? wallOrWallId : wallOrWallId?.id); return getEdges().filter((edge) => getEdgeWalls(edge).includes(wallId)); } function toFiniteNumber(value) { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; } function normalizeAngle(angle) { let next = angle; while (next > 180) { next -= 360; } while (next <= -180) { next += 360; } return next; } function setRotation(nextX, nextY) { state.rotationX = normalizeAngle(nextX); state.rotationY = normalizeAngle(nextY); } function snapRotationToWall(wallId) { const target = WALL_FRONT_ROTATIONS[normalizeId(wallId)]; if (!target) { return; } setRotation(target.x, target.y); } function facePoint(quad, u, v) { const weight0 = ((1 - u) * (1 - v)) / 4; const weight1 = ((1 + u) * (1 - v)) / 4; const weight2 = ((1 + u) * (1 + v)) / 4; const weight3 = ((1 - u) * (1 + v)) / 4; return { x: quad[0].x * weight0 + quad[1].x * weight1 + quad[2].x * weight2 + quad[3].x * weight3, y: quad[0].y * weight0 + quad[1].y * weight1 + quad[2].y * weight2 + quad[3].y * weight3 }; } function projectVerticesForRotation(rotationX, rotationY) { const yaw = (rotationY * Math.PI) / 180; const pitch = (rotationX * Math.PI) / 180; const cosY = Math.cos(yaw); const sinY = Math.sin(yaw); const cosX = Math.cos(pitch); const sinX = Math.sin(pitch); const centerX = CUBE_VIEW_CENTER.x; const centerY = CUBE_VIEW_CENTER.y; const scale = 54; const camera = 4.6; return CUBE_VERTICES.map(([x, y, z]) => { const x1 = x * cosY + z * sinY; const z1 = -x * sinY + z * cosY; const y2 = y * cosX - z1 * sinX; const z2 = y * sinX + z1 * cosX; const perspective = camera / (camera - z2); return { x: centerX + x1 * scale * perspective, y: centerY + y2 * scale * perspective, z: z2 }; }); } function projectVertices() { return projectVerticesForRotation(state.rotationX, state.rotationY); } function getEdgeGeometryById(edgeId) { const canonicalId = normalizeEdgeId(edgeId); const geometryIndex = EDGE_GEOMETRY_KEYS.indexOf(canonicalId); if (geometryIndex < 0) { return null; } return EDGE_GEOMETRY[geometryIndex] || null; } function getWallEdgeDirections(wallOrWallId) { const wallId = normalizeId(typeof wallOrWallId === "string" ? wallOrWallId : wallOrWallId?.id); const faceIndices = FACE_GEOMETRY[wallId]; if (!Array.isArray(faceIndices) || faceIndices.length !== 4) { return new Map(); } const frontRotation = WALL_FRONT_ROTATIONS[wallId] || { x: state.rotationX, y: state.rotationY }; const projectedVertices = projectVerticesForRotation(frontRotation.x, frontRotation.y); const quad = faceIndices.map((index) => projectedVertices[index]); const center = facePoint(quad, 0, 0); const directionsByEdgeId = new Map(); getEdgesForWall(wallId).forEach((edge) => { const geometry = getEdgeGeometryById(edge?.id); if (!geometry) { return; } const [fromIndex, toIndex] = geometry; const from = projectedVertices[fromIndex]; const to = projectedVertices[toIndex]; if (!from || !to) { return; } const midpointX = (from.x + to.x) / 2; const midpointY = (from.y + to.y) / 2; const dx = midpointX - center.x; const dy = midpointY - center.y; const directionByPosition = Math.abs(dx) >= Math.abs(dy) ? (dx >= 0 ? "east" : "west") : (dy >= 0 ? "south" : "north"); const direction = LOCAL_DIRECTION_VIEW_MAP[directionByPosition] || directionByPosition; directionsByEdgeId.set(normalizeEdgeId(edge?.id), direction); }); return directionsByEdgeId; } function getEdgeDirectionForWall(wallId, edgeId) { const wallKey = normalizeId(wallId); const edgeKey = normalizeEdgeId(edgeId); if (!wallKey || !edgeKey) { return ""; } const directions = getWallEdgeDirections(wallKey); return directions.get(edgeKey) || ""; } function getEdgeDirectionLabelForWall(wallId, edgeId) { return formatDirectionName(getEdgeDirectionForWall(wallId, edgeId)); } function bindRotationControls(elements) { if (state.controlsBound) { return; } const rotateAndRender = (deltaX, deltaY) => { setRotation(state.rotationX + deltaX, state.rotationY + deltaY); render(getElements()); }; elements.rotateLeftEl?.addEventListener("click", () => rotateAndRender(0, -9)); elements.rotateRightEl?.addEventListener("click", () => rotateAndRender(0, 9)); elements.rotateUpEl?.addEventListener("click", () => rotateAndRender(-9, 0)); elements.rotateDownEl?.addEventListener("click", () => rotateAndRender(9, 0)); elements.rotateResetEl?.addEventListener("click", () => { setRotation(18, -28); render(getElements()); }); elements.markerModeEl?.addEventListener("change", (event) => { const nextMode = normalizeId(event?.target?.value); state.markerDisplayMode = ["both", "letter", "astro", "tarot"].includes(nextMode) ? nextMode : "both"; render(getElements()); }); if (elements.connectorToggleEl) { elements.connectorToggleEl.checked = state.showConnectorLines; elements.connectorToggleEl.addEventListener("change", () => { state.showConnectorLines = Boolean(elements.connectorToggleEl.checked); if (!state.showConnectorLines && state.selectedNodeType === "connector") { state.selectedNodeType = "wall"; state.selectedConnectorId = null; } render(getElements()); }); } if (elements.primalToggleEl) { elements.primalToggleEl.checked = state.showPrimalPoint; elements.primalToggleEl.addEventListener("change", () => { state.showPrimalPoint = Boolean(elements.primalToggleEl.checked); if (!state.showPrimalPoint && state.selectedNodeType === "center") { state.selectedNodeType = "wall"; } render(getElements()); }); } state.controlsBound = true; } function getHebrewLetterSymbol(hebrewLetterId) { const id = normalizeLetterKey(hebrewLetterId); if (!id || !state.hebrewLetters) { return ""; } const entry = state.hebrewLetters[id]; if (!entry || typeof entry !== "object") { return ""; } const symbol = String( entry?.letter?.he || entry?.he || entry?.glyph || entry?.symbol || "" ).trim(); return symbol; } function getHebrewLetterName(hebrewLetterId) { const id = normalizeLetterKey(hebrewLetterId); if (!id || !state.hebrewLetters) { return ""; } const entry = state.hebrewLetters[id]; if (!entry || typeof entry !== "object") { return ""; } const name = String(entry?.letter?.name || entry?.name || "").trim(); return name; } function getAstrologySymbol(type, name) { const normalizedType = normalizeId(type); const normalizedName = normalizeId(name); const planetSymbols = { mercury: "☿︎", venus: "♀︎", mars: "♂︎", jupiter: "♃︎", saturn: "♄︎", sol: "☉︎", sun: "☉︎", luna: "☾︎", moon: "☾︎", earth: "⊕", uranus: "♅︎", neptune: "♆︎", pluto: "♇︎" }; const zodiacSymbols = { aries: "♈︎", taurus: "♉︎", gemini: "♊︎", cancer: "♋︎", leo: "♌︎", virgo: "♍︎", libra: "♎︎", scorpio: "♏︎", sagittarius: "♐︎", capricorn: "♑︎", aquarius: "♒︎", pisces: "♓︎" }; const elementSymbols = { fire: "🜂", water: "🜄", air: "🜁", earth: "🜃", spirit: "🜀" }; if (normalizedType === "planet") { return planetSymbols[normalizedName] || ""; } if (normalizedType === "zodiac") { return zodiacSymbols[normalizedName] || ""; } if (normalizedType === "element") { return elementSymbols[normalizedName] || ""; } return ""; } function getEdgeLetterId(edge) { return normalizeLetterKey(edge?.hebrewLetterId || edge?.associations?.hebrewLetterId); } function getWallFaceLetterId(wall) { return normalizeLetterKey(wall?.hebrewLetterId || wall?.associations?.hebrewLetterId); } function getWallFaceLetter(wall) { const hebrewLetterId = getWallFaceLetterId(wall); if (!hebrewLetterId) { return ""; } return getHebrewLetterSymbol(hebrewLetterId); } function getCubeCenterData() { const center = state.cube?.center; return center && typeof center === "object" ? center : null; } function getCenterLetterId(center = null) { const entry = center || getCubeCenterData(); return normalizeLetterKey(entry?.hebrewLetterId || entry?.associations?.hebrewLetterId || entry?.letter); } function getCenterLetterSymbol(center = null) { const centerLetterId = getCenterLetterId(center); if (!centerLetterId) { return ""; } return getHebrewLetterSymbol(centerLetterId); } function getConnectorById(connectorId) { const target = normalizeId(connectorId); return MOTHER_CONNECTORS.find((entry) => normalizeId(entry?.id) === target) || null; } function getConnectorPathEntry(connector) { const letterId = normalizeLetterKey(connector?.hebrewLetterId); if (!letterId) { return null; } return state.kabbalahPathsByLetterId.get(letterId) || null; } function getEdgePathEntry(edge) { const hebrewLetterId = getEdgeLetterId(edge); if (!hebrewLetterId) { return null; } return state.kabbalahPathsByLetterId.get(hebrewLetterId) || null; } function getEdgeAstrologySymbol(edge) { const pathEntry = getEdgePathEntry(edge); const astrology = pathEntry?.astrology || {}; return getAstrologySymbol(astrology.type, astrology.name); } function getEdgeMarkerDisplay(edge) { const letter = getEdgeLetter(edge); const astro = getEdgeAstrologySymbol(edge); if (state.markerDisplayMode === "letter") { return letter ? { text: letter, isMissing: false } : { text: "!", isMissing: true }; } if (state.markerDisplayMode === "astro") { return astro ? { text: astro, isMissing: false } : { text: "!", isMissing: true }; } if (letter && astro) { return { text: `${letter} ${astro}`, isMissing: false }; } return { text: "!", isMissing: true }; } function getEdgeLetter(edge) { const hebrewLetterId = getEdgeLetterId(edge); if (!hebrewLetterId) { return ""; } return getHebrewLetterSymbol(hebrewLetterId); } function getWallTarotCard(wall) { return toDisplayText(wall?.associations?.tarotCard || wall?.tarotCard); } function getEdgeTarotCard(edge) { const pathEntry = getEdgePathEntry(edge); return toDisplayText(pathEntry?.tarot?.card); } function getConnectorTarotCard(connector) { const pathEntry = getConnectorPathEntry(connector); return toDisplayText(pathEntry?.tarot?.card); } function getCenterTarotCard(center = null) { const entry = center || getCubeCenterData(); return toDisplayText(entry?.associations?.tarotCard || entry?.tarotCard); } function resolveCardImageUrl(cardName) { const name = toDisplayText(cardName); if (!name || typeof window.TarotCardImages?.resolveTarotCardImage !== "function") { return null; } return window.TarotCardImages.resolveTarotCardImage(name) || null; } function applyPlacement(placement) { const fallbackWallId = normalizeId(getWalls()[0]?.id); const nextWallId = normalizeId(placement?.wallId || placement?.wall?.id || state.selectedWallId || fallbackWallId); const wall = getWallById(nextWallId); if (!wall) { return false; } state.selectedWallId = normalizeId(wall.id); const candidateEdgeId = normalizeEdgeId(placement?.edgeId || placement?.edge?.id); const wallEdges = getEdgesForWall(state.selectedWallId); const resolvedEdgeId = candidateEdgeId && getEdgeById(candidateEdgeId) ? candidateEdgeId : normalizeEdgeId(wallEdges[0]?.id || getEdges()[0]?.id); state.selectedEdgeId = resolvedEdgeId; state.selectedNodeType = "wall"; state.selectedConnectorId = null; render(getElements()); return true; } function createMetaCard(title, bodyContent) { const card = document.createElement("div"); card.className = "planet-meta-card"; const titleEl = document.createElement("strong"); titleEl.textContent = title; card.appendChild(titleEl); if (typeof bodyContent === "string") { const bodyEl = document.createElement("p"); bodyEl.className = "planet-text"; bodyEl.textContent = bodyContent; card.appendChild(bodyEl); } else if (bodyContent instanceof Node) { card.appendChild(bodyContent); } return card; } function createNavButton(label, eventName, detail) { const button = document.createElement("button"); button.type = "button"; button.className = "kab-god-link"; button.textContent = `${label} ↗`; button.addEventListener("click", () => { document.dispatchEvent(new CustomEvent(eventName, { detail })); }); return button; } function toDisplayText(value) { return String(value ?? "").trim(); } function escapeHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } function toDetailValueMarkup(value) { const text = toDisplayText(value); return text ? escapeHtml(text) : '!'; } function renderFaceSvg(containerEl, walls) { if (!containerEl) { return; } const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("viewBox", "0 0 240 220"); svg.setAttribute("width", "100%"); svg.setAttribute("class", "cube-svg"); svg.setAttribute("role", "img"); svg.setAttribute("aria-label", "Cube of Space interactive chassis"); const wallById = new Map(walls.map((wall) => [normalizeId(wall?.id), wall])); const projectedVertices = projectVertices(); const faces = Object.entries(FACE_GEOMETRY) .map(([wallId, indices]) => { const wall = wallById.get(wallId); if (!wall) { return null; } const quad = indices.map((index) => projectedVertices[index]); const avgDepth = quad.reduce((sum, point) => sum + point.z, 0) / quad.length; return { wallId, wall, quad, depth: avgDepth, pointsText: quad.map((point) => `${point.x.toFixed(2)},${point.y.toFixed(2)}`).join(" ") }; }) .filter(Boolean) .sort((left, right) => left.depth - right.depth); faces.forEach((faceData) => { const { wallId, wall, quad, pointsText } = faceData; const isActive = wallId === normalizeId(state.selectedWallId); const polygon = document.createElementNS(svgNS, "polygon"); polygon.setAttribute("points", pointsText); polygon.setAttribute("class", `cube-face${isActive ? " is-active" : ""}`); polygon.setAttribute("fill", "#000"); polygon.setAttribute("fill-opacity", isActive ? "0.78" : "0.62"); polygon.setAttribute("stroke", "currentColor"); polygon.setAttribute("stroke-opacity", isActive ? "0.92" : "0.68"); polygon.setAttribute("stroke-width", isActive ? "2.5" : "1"); polygon.setAttribute("data-wall-id", wallId); polygon.setAttribute("role", "button"); polygon.setAttribute("tabindex", "0"); polygon.setAttribute("aria-label", `Cube wall ${wall?.name || wallId}`); const selectWall = () => { state.selectedWallId = wallId; state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(wallId)[0]?.id || getEdges()[0]?.id); state.selectedNodeType = "wall"; state.selectedConnectorId = null; snapRotationToWall(wallId); render(getElements()); }; polygon.addEventListener("click", selectWall); polygon.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectWall(); } }); svg.appendChild(polygon); const wallFaceLetter = getWallFaceLetter(wall); const faceGlyphAnchor = facePoint(quad, 0, 0); if (state.markerDisplayMode === "tarot") { const cardUrl = resolveCardImageUrl(getWallTarotCard(wall)); if (cardUrl) { let defs = svg.querySelector("defs"); if (!defs) { defs = document.createElementNS(svgNS, "defs"); svg.insertBefore(defs, svg.firstChild); } const clipId = `face-clip-${wallId}`; const clipPath = document.createElementNS(svgNS, "clipPath"); clipPath.setAttribute("id", clipId); const clipPoly = document.createElementNS(svgNS, "polygon"); clipPoly.setAttribute("points", pointsText); clipPath.appendChild(clipPoly); defs.appendChild(clipPath); const cardW = 40, cardH = 60; const cardImg = document.createElementNS(svgNS, "image"); cardImg.setAttribute("href", cardUrl); cardImg.setAttribute("x", String((faceGlyphAnchor.x - cardW / 2).toFixed(2))); cardImg.setAttribute("y", String((faceGlyphAnchor.y - cardH / 2).toFixed(2))); cardImg.setAttribute("width", String(cardW)); cardImg.setAttribute("height", String(cardH)); cardImg.setAttribute("clip-path", `url(#${clipId})`); cardImg.setAttribute("role", "button"); cardImg.setAttribute("tabindex", "0"); cardImg.setAttribute("aria-label", `Cube wall ${wall?.name || wallId}`); cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet"); cardImg.addEventListener("click", selectWall); cardImg.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectWall(); } }); svg.appendChild(cardImg); } } else { const faceGlyph = document.createElementNS(svgNS, "text"); faceGlyph.setAttribute( "class", `cube-face-symbol${isActive ? " is-active" : ""}${wallFaceLetter ? "" : " is-missing"}` ); faceGlyph.setAttribute("x", String(faceGlyphAnchor.x)); faceGlyph.setAttribute("y", String(faceGlyphAnchor.y)); faceGlyph.setAttribute("text-anchor", "middle"); faceGlyph.setAttribute("dominant-baseline", "middle"); faceGlyph.setAttribute("pointer-events", "none"); faceGlyph.textContent = wallFaceLetter || "!"; svg.appendChild(faceGlyph); } const labelAnchor = facePoint(quad, 0, 0.9); const label = document.createElementNS(svgNS, "text"); label.setAttribute("class", `cube-face-label${isActive ? " is-active" : ""}`); label.setAttribute("x", String(labelAnchor.x)); label.setAttribute("y", String(labelAnchor.y)); label.setAttribute("text-anchor", "middle"); label.setAttribute("dominant-baseline", "middle"); label.setAttribute("pointer-events", "none"); label.textContent = wall?.name || wallId; svg.appendChild(label); }); const faceCenterByWallId = new Map( faces.map((faceData) => [faceData.wallId, facePoint(faceData.quad, 0, 0)]) ); if (state.showConnectorLines) { MOTHER_CONNECTORS.forEach((connector, connectorIndex) => { const fromWallId = normalizeId(connector?.fromWallId); const toWallId = normalizeId(connector?.toWallId); const from = faceCenterByWallId.get(fromWallId); const to = faceCenterByWallId.get(toWallId); if (!from || !to) { return; } const connectorId = normalizeId(connector?.id); const isActive = state.selectedNodeType === "connector" && normalizeId(state.selectedConnectorId) === connectorId; const connectorLetter = getHebrewLetterSymbol(connector?.hebrewLetterId); const connectorCardUrl = state.markerDisplayMode === "tarot" ? resolveCardImageUrl(getConnectorTarotCard(connector)) : null; const group = document.createElementNS(svgNS, "g"); group.setAttribute("class", `cube-connector${isActive ? " is-active" : ""}`); group.setAttribute("role", "button"); group.setAttribute("tabindex", "0"); group.setAttribute( "aria-label", `Mother connector ${formatDirectionName(fromWallId)} to ${formatDirectionName(toWallId)}` ); const connectorLine = document.createElementNS(svgNS, "line"); connectorLine.setAttribute("class", `cube-connector-line${isActive ? " is-active" : ""}`); connectorLine.setAttribute("x1", from.x.toFixed(2)); connectorLine.setAttribute("y1", from.y.toFixed(2)); connectorLine.setAttribute("x2", to.x.toFixed(2)); connectorLine.setAttribute("y2", to.y.toFixed(2)); group.appendChild(connectorLine); const connectorHit = document.createElementNS(svgNS, "line"); connectorHit.setAttribute("class", "cube-connector-hit"); connectorHit.setAttribute("x1", from.x.toFixed(2)); connectorHit.setAttribute("y1", from.y.toFixed(2)); connectorHit.setAttribute("x2", to.x.toFixed(2)); connectorHit.setAttribute("y2", to.y.toFixed(2)); group.appendChild(connectorHit); const dx = to.x - from.x; const dy = to.y - from.y; const length = Math.hypot(dx, dy) || 1; const perpX = -dy / length; const perpY = dx / length; const shift = (connectorIndex - 1) * 12; const labelX = ((from.x + to.x) / 2) + (perpX * shift); const labelY = ((from.y + to.y) / 2) + (perpY * shift); if (state.markerDisplayMode === "tarot" && connectorCardUrl) { const cardW = 18; const cardH = 27; const connectorImg = document.createElementNS(svgNS, "image"); connectorImg.setAttribute("href", connectorCardUrl); connectorImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2))); connectorImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2))); connectorImg.setAttribute("width", String(cardW)); connectorImg.setAttribute("height", String(cardH)); connectorImg.setAttribute("preserveAspectRatio", "xMidYMid meet"); group.appendChild(connectorImg); } else { const connectorText = document.createElementNS(svgNS, "text"); connectorText.setAttribute( "class", `cube-connector-symbol${isActive ? " is-active" : ""}${connectorLetter ? "" : " is-missing"}` ); connectorText.setAttribute("x", String(labelX)); connectorText.setAttribute("y", String(labelY)); connectorText.setAttribute("text-anchor", "middle"); connectorText.setAttribute("dominant-baseline", "middle"); connectorText.setAttribute("pointer-events", "none"); connectorText.textContent = connectorLetter || "!"; group.appendChild(connectorText); } const selectConnector = () => { state.selectedNodeType = "connector"; state.selectedConnectorId = connectorId; render(getElements()); }; group.addEventListener("click", selectConnector); group.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectConnector(); } }); svg.appendChild(group); }); } const edgeById = new Map( getEdges().map((edge) => [normalizeEdgeId(edge?.id), edge]) ); EDGE_GEOMETRY.forEach(([fromIndex, toIndex], edgeIndex) => { const edgeId = EDGE_GEOMETRY_KEYS[edgeIndex]; const edge = edgeById.get(edgeId) || { id: edgeId, name: formatEdgeName(edgeId), walls: edgeId.split("-") }; const markerDisplay = getEdgeMarkerDisplay(edge); const edgeWalls = getEdgeWalls(edge); const wallIsActive = edgeWalls.includes(normalizeId(state.selectedWallId)); const edgeIsActive = normalizeEdgeId(state.selectedEdgeId) === edgeId; const from = projectedVertices[fromIndex]; const to = projectedVertices[toIndex]; const line = document.createElementNS(svgNS, "line"); line.setAttribute("x1", from.x.toFixed(2)); line.setAttribute("y1", from.y.toFixed(2)); line.setAttribute("x2", to.x.toFixed(2)); line.setAttribute("y2", to.y.toFixed(2)); line.setAttribute("stroke", "currentColor"); line.setAttribute("stroke-opacity", edgeIsActive ? "0.94" : (wallIsActive ? "0.70" : "0.32")); line.setAttribute("stroke-width", edgeIsActive ? "2.4" : (wallIsActive ? "1.9" : "1.4")); line.setAttribute("class", `cube-edge-line${edgeIsActive ? " is-active" : ""}`); line.setAttribute("role", "button"); line.setAttribute("tabindex", "0"); line.setAttribute("aria-label", `Cube edge ${toDisplayText(edge?.name) || formatEdgeName(edgeId)}`); const selectEdge = () => { state.selectedEdgeId = edgeId; state.selectedNodeType = "wall"; state.selectedConnectorId = null; if (!edgeWalls.includes(normalizeId(state.selectedWallId)) && edgeWalls[0]) { state.selectedWallId = edgeWalls[0]; snapRotationToWall(state.selectedWallId); } render(getElements()); }; line.addEventListener("click", selectEdge); line.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectEdge(); } }); svg.appendChild(line); const dx = to.x - from.x; const dy = to.y - from.y; const length = Math.hypot(dx, dy) || 1; const normalX = -dy / length; const normalY = dx / length; const midpointX = (from.x + to.x) / 2; const midpointY = (from.y + to.y) / 2; const centerVectorX = midpointX - CUBE_VIEW_CENTER.x; const centerVectorY = midpointY - CUBE_VIEW_CENTER.y; const normalSign = (centerVectorX * normalX + centerVectorY * normalY) >= 0 ? 1 : -1; const markerOffset = edgeIsActive ? 17 : (wallIsActive ? 13 : 12); const labelX = midpointX + (normalX * markerOffset * normalSign); const labelY = midpointY + (normalY * markerOffset * normalSign); const marker = document.createElementNS(svgNS, "g"); marker.setAttribute( "class", `cube-direction${wallIsActive ? " is-wall-active" : ""}${edgeIsActive ? " is-active" : ""}` ); marker.setAttribute("role", "button"); marker.setAttribute("tabindex", "0"); marker.setAttribute("aria-label", `Cube edge ${toDisplayText(edge?.name) || formatEdgeName(edgeId)}`); if (state.markerDisplayMode === "tarot") { const edgeCardUrl = resolveCardImageUrl(getEdgeTarotCard(edge)); if (edgeCardUrl) { const cardW = edgeIsActive ? 28 : 20; const cardH = edgeIsActive ? 42 : 30; const cardImg = document.createElementNS(svgNS, "image"); cardImg.setAttribute("class", `cube-direction-card${edgeIsActive ? " is-active" : ""}`); cardImg.setAttribute("href", edgeCardUrl); cardImg.setAttribute("x", String((labelX - cardW / 2).toFixed(2))); cardImg.setAttribute("y", String((labelY - cardH / 2).toFixed(2))); cardImg.setAttribute("width", String(cardW)); cardImg.setAttribute("height", String(cardH)); cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet"); marker.appendChild(cardImg); } else { const markerText = document.createElementNS(svgNS, "text"); markerText.setAttribute("class", "cube-direction-letter is-missing"); markerText.setAttribute("x", String(labelX)); markerText.setAttribute("y", String(labelY)); markerText.setAttribute("text-anchor", "middle"); markerText.setAttribute("dominant-baseline", "middle"); markerText.textContent = "!"; marker.appendChild(markerText); } } else { const markerText = document.createElementNS(svgNS, "text"); markerText.setAttribute( "class", `cube-direction-letter${markerDisplay.isMissing ? " is-missing" : ""}` ); markerText.setAttribute("x", String(labelX)); markerText.setAttribute("y", String(labelY)); markerText.setAttribute("text-anchor", "middle"); markerText.setAttribute("dominant-baseline", "middle"); markerText.textContent = markerDisplay.text; marker.appendChild(markerText); } marker.addEventListener("click", selectEdge); marker.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectEdge(); } }); svg.appendChild(marker); }); const center = getCubeCenterData(); if (center && state.showPrimalPoint) { const centerLetter = getCenterLetterSymbol(center); const centerCardUrl = state.markerDisplayMode === "tarot" ? resolveCardImageUrl(getCenterTarotCard(center)) : null; const centerActive = state.selectedNodeType === "center"; const centerMarker = document.createElementNS(svgNS, "g"); centerMarker.setAttribute("class", `cube-center${centerActive ? " is-active" : ""}`); centerMarker.setAttribute("role", "button"); centerMarker.setAttribute("tabindex", "0"); centerMarker.setAttribute("aria-label", "Cube primal point"); const centerHit = document.createElementNS(svgNS, "circle"); centerHit.setAttribute("class", "cube-center-hit"); centerHit.setAttribute("cx", String(CUBE_VIEW_CENTER.x)); centerHit.setAttribute("cy", String(CUBE_VIEW_CENTER.y)); centerHit.setAttribute("r", "18"); centerMarker.appendChild(centerHit); if (state.markerDisplayMode === "tarot" && centerCardUrl) { const cardW = 24; const cardH = 36; const centerImg = document.createElementNS(svgNS, "image"); centerImg.setAttribute("href", centerCardUrl); centerImg.setAttribute("x", String((CUBE_VIEW_CENTER.x - cardW / 2).toFixed(2))); centerImg.setAttribute("y", String((CUBE_VIEW_CENTER.y - cardH / 2).toFixed(2))); centerImg.setAttribute("width", String(cardW)); centerImg.setAttribute("height", String(cardH)); centerImg.setAttribute("preserveAspectRatio", "xMidYMid meet"); centerMarker.appendChild(centerImg); } else { const centerText = document.createElementNS(svgNS, "text"); centerText.setAttribute( "class", `cube-center-symbol${centerActive ? " is-active" : ""}${centerLetter ? "" : " is-missing"}` ); centerText.setAttribute("x", String(CUBE_VIEW_CENTER.x)); centerText.setAttribute("y", String(CUBE_VIEW_CENTER.y)); centerText.setAttribute("text-anchor", "middle"); centerText.setAttribute("dominant-baseline", "middle"); centerText.setAttribute("pointer-events", "none"); centerText.textContent = centerLetter || "!"; centerMarker.appendChild(centerText); } const selectCenter = () => { state.selectedNodeType = "center"; state.selectedConnectorId = null; render(getElements()); }; centerMarker.addEventListener("click", selectCenter); centerMarker.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectCenter(); } }); svg.appendChild(centerMarker); } if (state.markerDisplayMode === "tarot") { Array.from(svg.querySelectorAll("g.cube-direction")).forEach((group) => { svg.appendChild(group); }); if (state.showConnectorLines) { Array.from(svg.querySelectorAll("g.cube-connector")).forEach((group) => { svg.appendChild(group); }); } } containerEl.replaceChildren(svg); } function renderCenterDetail(elements) { if (!state.showPrimalPoint) { return false; } const center = getCubeCenterData(); if (!center || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) { return false; } const centerLetterId = getCenterLetterId(center); const centerLetter = getCenterLetterSymbol(center); const centerLetterText = centerLetterId ? `${centerLetter ? `${centerLetter} ` : ""}${toDisplayText(centerLetterId)}` : ""; const centerElement = toDisplayText(center?.element); elements.detailNameEl.textContent = "Primal Point"; elements.detailSubEl.textContent = [centerLetterText, centerElement].filter(Boolean).join(" · ") || "Center of the Cube"; const bodyEl = elements.detailBodyEl; bodyEl.innerHTML = ""; const summary = document.createElement("div"); summary.className = "planet-text"; summary.innerHTML = `