(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 = `
Name
${toDetailValueMarkup(center?.name)}
Letter
${toDetailValueMarkup(centerLetterText)}
Element
${toDetailValueMarkup(center?.element)}
`; bodyEl.appendChild(createMetaCard("Center Details", summary)); if (Array.isArray(center?.keywords) && center.keywords.length) { bodyEl.appendChild(createMetaCard("Keywords", center.keywords.join(", "))); } if (center?.description) { bodyEl.appendChild(createMetaCard("Description", center.description)); } const associations = center?.associations || {}; const links = document.createElement("div"); links.className = "kab-god-links"; if (centerLetterId) { links.appendChild(createNavButton(centerLetter || "!", "nav:alphabet", { alphabet: "hebrew", hebrewLetterId: centerLetterId })); } const centerTrumpNo = toFiniteNumber(associations?.tarotTrumpNumber); const centerTarotCard = toDisplayText(associations?.tarotCard); if (centerTarotCard || centerTrumpNo != null) { links.appendChild(createNavButton(centerTarotCard || `Trump ${centerTrumpNo}`, "nav:tarot-trump", { cardName: centerTarotCard, trumpNumber: centerTrumpNo })); } const centerPathNo = toFiniteNumber(associations?.kabbalahPathNumber); if (centerPathNo != null) { links.appendChild(createNavButton(`Path ${centerPathNo}`, "nav:kabbalah-path", { pathNo: centerPathNo })); } if (links.childElementCount) { const linksCard = document.createElement("div"); linksCard.className = "planet-meta-card"; linksCard.innerHTML = "Correspondence Links"; linksCard.appendChild(links); bodyEl.appendChild(linksCard); } return true; } function renderConnectorDetail(elements, walls) { const connector = getConnectorById(state.selectedConnectorId); if (!connector || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) { return false; } const fromWallId = normalizeId(connector?.fromWallId); const toWallId = normalizeId(connector?.toWallId); const fromWall = getWallById(fromWallId) || walls.find((entry) => normalizeId(entry?.id) === fromWallId) || null; const toWall = getWallById(toWallId) || walls.find((entry) => normalizeId(entry?.id) === toWallId) || null; const connectorPath = getConnectorPathEntry(connector); const letterId = normalizeLetterKey(connector?.hebrewLetterId); const letterSymbol = getHebrewLetterSymbol(letterId); const letterText = letterId ? `${letterSymbol ? `${letterSymbol} ` : ""}${toDisplayText(letterId)}` : ""; const pathNo = toFiniteNumber(connectorPath?.pathNumber); const tarotCard = toDisplayText(connectorPath?.tarot?.card); const tarotTrumpNumber = toFiniteNumber(connectorPath?.tarot?.trumpNumber); const astrologyType = toDisplayText(connectorPath?.astrology?.type); const astrologyName = toDisplayText(connectorPath?.astrology?.name); const astrologySummary = [astrologyType, astrologyName].filter(Boolean).join(": "); elements.detailNameEl.textContent = connector?.name || "Mother Connector"; elements.detailSubEl.textContent = ["Mother Letter", letterText].filter(Boolean).join(" · ") || "Mother Letter"; const bodyEl = elements.detailBodyEl; bodyEl.innerHTML = ""; const summary = document.createElement("div"); summary.className = "planet-text"; summary.innerHTML = `
Letter
${toDetailValueMarkup(letterText)}
From
${toDetailValueMarkup(fromWall?.name || formatDirectionName(fromWallId))}
To
${toDetailValueMarkup(toWall?.name || formatDirectionName(toWallId))}
Tarot
${toDetailValueMarkup(tarotCard || (tarotTrumpNumber != null ? `Trump ${tarotTrumpNumber}` : ""))}
`; bodyEl.appendChild(createMetaCard("Connector Details", summary)); if (astrologySummary) { bodyEl.appendChild(createMetaCard("Astrology", astrologySummary)); } const links = document.createElement("div"); links.className = "kab-god-links"; if (letterId) { links.appendChild(createNavButton(letterSymbol || "!", "nav:alphabet", { alphabet: "hebrew", hebrewLetterId: letterId })); } if (pathNo != null) { links.appendChild(createNavButton(`Path ${pathNo}`, "nav:kabbalah-path", { pathNo })); } if (tarotCard || tarotTrumpNumber != null) { links.appendChild(createNavButton(tarotCard || `Trump ${tarotTrumpNumber}`, "nav:tarot-trump", { cardName: tarotCard, trumpNumber: tarotTrumpNumber })); } if (links.childElementCount) { const linksCard = document.createElement("div"); linksCard.className = "planet-meta-card"; linksCard.innerHTML = "Correspondence Links"; linksCard.appendChild(links); bodyEl.appendChild(linksCard); } return true; } function renderEdgeCard(wall, detailBodyEl, wallEdgeDirections = new Map()) { const wallId = normalizeId(wall?.id); const selectedEdge = getEdgeById(state.selectedEdgeId) || getEdgesForWall(wallId)[0] || getEdges()[0] || null; if (!selectedEdge) { return; } state.selectedEdgeId = normalizeEdgeId(selectedEdge.id); const edgeDirection = wallEdgeDirections.get(normalizeEdgeId(selectedEdge.id)); const edgeName = edgeDirection ? formatDirectionName(edgeDirection) : (toDisplayText(selectedEdge.name) || formatEdgeName(selectedEdge.id)); const edgeWalls = getEdgeWalls(selectedEdge) .map((entry) => entry.charAt(0).toUpperCase() + entry.slice(1)) .join(" · "); const edgeLetterId = getEdgeLetterId(selectedEdge); const edgeLetter = getEdgeLetter(selectedEdge); const edgePath = getEdgePathEntry(selectedEdge); const astrologyType = toDisplayText(edgePath?.astrology?.type); const astrologyName = toDisplayText(edgePath?.astrology?.name); const astrologySymbol = getEdgeAstrologySymbol(selectedEdge); const astrologyText = astrologySymbol && astrologyName ? `${astrologySymbol} ${astrologyName}` : astrologySymbol || astrologyName; const pathNo = toFiniteNumber(edgePath?.pathNumber); const tarotCard = toDisplayText(edgePath?.tarot?.card); const tarotTrumpNumber = toFiniteNumber(edgePath?.tarot?.trumpNumber); const edgeCard = document.createElement("div"); edgeCard.className = "planet-meta-card"; const title = document.createElement("strong"); title.textContent = `Edge · ${edgeName}`; edgeCard.appendChild(title); const dlWrap = document.createElement("div"); dlWrap.className = "planet-text"; dlWrap.innerHTML = `
Direction
${toDetailValueMarkup(edgeName)}
Edge
${toDetailValueMarkup(edgeWalls)}
Letter
${toDetailValueMarkup(edgeLetter)}
Astrology
${toDetailValueMarkup(astrologyText)}
Tarot
${toDetailValueMarkup(tarotCard)}
`; edgeCard.appendChild(dlWrap); if (Array.isArray(selectedEdge.keywords) && selectedEdge.keywords.length) { const keywords = document.createElement("p"); keywords.className = "planet-text"; keywords.textContent = selectedEdge.keywords.join(", "); edgeCard.appendChild(keywords); } if (selectedEdge.description) { const description = document.createElement("p"); description.className = "planet-text"; description.textContent = selectedEdge.description; edgeCard.appendChild(description); } const links = document.createElement("div"); links.className = "kab-god-links"; if (edgeLetterId) { links.appendChild(createNavButton(edgeLetter || "!", "nav:alphabet", { alphabet: "hebrew", hebrewLetterId: edgeLetterId })); } if (astrologyType === "zodiac" && astrologyName) { links.appendChild(createNavButton(astrologyName, "nav:zodiac", { signId: normalizeId(astrologyName) })); } if (tarotCard) { links.appendChild(createNavButton(tarotCard, "nav:tarot-trump", { cardName: tarotCard, trumpNumber: tarotTrumpNumber })); } if (pathNo != null) { links.appendChild(createNavButton(`Path ${pathNo}`, "nav:kabbalah-path", { pathNo })); } if (links.childElementCount) { edgeCard.appendChild(links); } detailBodyEl.appendChild(edgeCard); } function renderDetail(elements, walls) { if (state.selectedNodeType === "connector" && renderConnectorDetail(elements, walls)) { return; } if (state.selectedNodeType === "center" && renderCenterDetail(elements)) { return; } const wall = getWallById(state.selectedWallId) || walls[0] || null; if (!wall || !elements?.detailNameEl || !elements?.detailSubEl || !elements?.detailBodyEl) { if (elements?.detailNameEl) { elements.detailNameEl.textContent = "Cube data unavailable"; } if (elements?.detailSubEl) { elements.detailSubEl.textContent = "Could not load cube dataset."; } if (elements?.detailBodyEl) { elements.detailBodyEl.innerHTML = ""; } return; } state.selectedWallId = normalizeId(wall.id); const wallPlanet = toDisplayText(wall?.planet) || "!"; const wallElement = toDisplayText(wall?.element) || "!"; const wallFaceLetterId = getWallFaceLetterId(wall); const wallFaceLetter = getWallFaceLetter(wall); const wallFaceLetterText = wallFaceLetterId ? `${wallFaceLetter ? `${wallFaceLetter} ` : ""}${toDisplayText(wallFaceLetterId)}` : ""; elements.detailNameEl.textContent = `${wall.name} Wall`; elements.detailSubEl.textContent = `${wallElement} · ${wallPlanet}`; const bodyEl = elements.detailBodyEl; bodyEl.innerHTML = ""; const summary = document.createElement("div"); summary.className = "planet-text"; summary.innerHTML = `
Opposite
${toDetailValueMarkup(wall.opposite)}
Face Letter
${toDetailValueMarkup(wallFaceLetterText)}
Element
${toDetailValueMarkup(wall.element)}
Planet
${toDetailValueMarkup(wall.planet)}
Archangel
${toDetailValueMarkup(wall.archangel)}
`; bodyEl.appendChild(createMetaCard("Wall Details", summary)); if (Array.isArray(wall.keywords) && wall.keywords.length) { bodyEl.appendChild(createMetaCard("Keywords", wall.keywords.join(", "))); } if (wall.description) { bodyEl.appendChild(createMetaCard("Description", wall.description)); } const wallLinksCard = document.createElement("div"); wallLinksCard.className = "planet-meta-card"; wallLinksCard.innerHTML = "Correspondence Links"; const wallLinks = document.createElement("div"); wallLinks.className = "kab-god-links"; if (wallFaceLetterId) { const wallFaceLetterName = getHebrewLetterName(wallFaceLetterId) || toDisplayText(wallFaceLetterId); const faceLetterText = [wallFaceLetter, wallFaceLetterName].filter(Boolean).join(" "); const faceLetterLabel = faceLetterText ? `Face ${faceLetterText}` : "Face !"; wallLinks.appendChild(createNavButton(faceLetterLabel, "nav:alphabet", { alphabet: "hebrew", hebrewLetterId: wallFaceLetterId })); } const wallAssociations = wall.associations || {}; if (wallAssociations.planetId) { wallLinks.appendChild(createNavButton(toDisplayText(wall.planet) || "!", "nav:planet", { planetId: wallAssociations.planetId })); } if (wallAssociations.godName) { wallLinks.appendChild(createNavButton(wallAssociations.godName, "nav:gods", { godName: wallAssociations.godName })); } if (wall.oppositeWallId) { const oppositeWall = getWallById(wall.oppositeWallId); const internal = document.createElement("button"); internal.type = "button"; internal.className = "kab-god-link"; internal.textContent = `Opposite: ${oppositeWall?.name || wall.oppositeWallId}`; internal.addEventListener("click", () => { state.selectedWallId = normalizeId(wall.oppositeWallId); state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(state.selectedWallId)[0]?.id || getEdges()[0]?.id); state.selectedNodeType = "wall"; state.selectedConnectorId = null; snapRotationToWall(state.selectedWallId); render(getElements()); }); wallLinks.appendChild(internal); } if (wallLinks.childElementCount) { wallLinksCard.appendChild(wallLinks); bodyEl.appendChild(wallLinksCard); } const edgesCard = document.createElement("div"); edgesCard.className = "planet-meta-card"; edgesCard.innerHTML = "Wall Edges"; const chips = document.createElement("div"); chips.className = "kab-chips"; const wallEdgeDirections = getWallEdgeDirections(wall); const wallEdges = getEdgesForWall(wall) .slice() .sort((left, right) => { const leftDirection = wallEdgeDirections.get(normalizeEdgeId(left?.id)); const rightDirection = wallEdgeDirections.get(normalizeEdgeId(right?.id)); const leftRank = LOCAL_DIRECTION_RANK[leftDirection] ?? LOCAL_DIRECTION_ORDER.length; const rightRank = LOCAL_DIRECTION_RANK[rightDirection] ?? LOCAL_DIRECTION_ORDER.length; if (leftRank !== rightRank) { return leftRank - rightRank; } return normalizeEdgeId(left?.id).localeCompare(normalizeEdgeId(right?.id)); }); wallEdges.forEach((edge) => { const id = normalizeEdgeId(edge.id); const chipLetter = getEdgeLetter(edge); const chipIsMissing = !chipLetter; const direction = wallEdgeDirections.get(id); const directionLabel = direction ? formatDirectionName(direction) : (toDisplayText(edge.name) || formatEdgeName(edge.id)); const chip = document.createElement("span"); chip.className = `kab-chip${id === normalizeEdgeId(state.selectedEdgeId) ? " is-active" : ""}${chipIsMissing ? " is-missing" : ""}`; chip.setAttribute("role", "button"); chip.setAttribute("tabindex", "0"); chip.textContent = `${directionLabel} · ${chipLetter || "!"}`; const selectEdge = () => { state.selectedEdgeId = id; state.selectedNodeType = "wall"; state.selectedConnectorId = null; render(getElements()); }; chip.addEventListener("click", selectEdge); chip.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectEdge(); } }); chips.appendChild(chip); }); edgesCard.appendChild(chips); bodyEl.appendChild(edgesCard); renderEdgeCard(wall, bodyEl, wallEdgeDirections); } function render(elements) { if (elements?.markerModeEl) { elements.markerModeEl.value = state.markerDisplayMode; } if (elements?.connectorToggleEl) { elements.connectorToggleEl.checked = state.showConnectorLines; } if (elements?.primalToggleEl) { elements.primalToggleEl.checked = state.showPrimalPoint; } if (elements?.rotationReadoutEl) { elements.rotationReadoutEl.textContent = `X ${Math.round(state.rotationX)}° · Y ${Math.round(state.rotationY)}°`; } const walls = getWalls(); renderFaceSvg(elements.viewContainerEl, walls); renderDetail(elements, walls); } function ensureCubeSection(magickDataset) { const cubeData = magickDataset?.grouped?.kabbalah?.cube; const elements = getElements(); state.cube = cubeData || null; state.hebrewLetters = asRecord(magickDataset?.grouped?.hebrewLetters) || asRecord(magickDataset?.grouped?.alphabets?.hebrew) || null; const pathList = Array.isArray(magickDataset?.grouped?.kabbalah?.["kabbalah-tree"]?.paths) ? magickDataset.grouped.kabbalah["kabbalah-tree"].paths : []; const letterEntries = state.hebrewLetters && typeof state.hebrewLetters === "object" ? Object.values(state.hebrewLetters) : []; const letterIdsByChar = new Map( letterEntries .map((letterEntry) => [String(letterEntry?.letter?.he || "").trim(), normalizeLetterKey(letterEntry?.id)]) .filter(([character, letterId]) => Boolean(character) && Boolean(letterId)) ); state.kabbalahPathsByLetterId = new Map( pathList .map((pathEntry) => { const transliterationId = normalizeLetterKey(pathEntry?.hebrewLetter?.transliteration); const char = String(pathEntry?.hebrewLetter?.char || "").trim(); const charId = letterIdsByChar.get(char) || ""; return [charId || transliterationId, pathEntry]; }) .filter(([letterId]) => Boolean(letterId)) ); if (!state.selectedWallId) { state.selectedWallId = normalizeId(getWalls()[0]?.id); } const initialEdge = getEdgesForWall(state.selectedWallId)[0] || getEdges()[0] || null; if (!state.selectedEdgeId || !getEdgeById(state.selectedEdgeId)) { state.selectedEdgeId = normalizeEdgeId(initialEdge?.id); } bindRotationControls(elements); render(elements); state.initialized = true; } function selectWallById(wallId) { if (!state.initialized) { return false; } const wall = getWallById(wallId); if (!wall) { return false; } state.selectedWallId = normalizeId(wall.id); state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(state.selectedWallId)[0]?.id || getEdges()[0]?.id); state.selectedNodeType = "wall"; state.selectedConnectorId = null; snapRotationToWall(state.selectedWallId); render(getElements()); return true; } function selectConnectorById(connectorId) { if (!state.initialized) { return false; } const connector = getConnectorById(connectorId); if (!connector) { return false; } const fromWallId = normalizeId(connector.fromWallId); if (fromWallId && getWallById(fromWallId)) { state.selectedWallId = fromWallId; state.selectedEdgeId = normalizeEdgeId(getEdgesForWall(fromWallId)[0]?.id || getEdges()[0]?.id); snapRotationToWall(fromWallId); } state.showConnectorLines = true; state.selectedNodeType = "connector"; state.selectedConnectorId = normalizeId(connector.id); render(getElements()); return true; } function selectCenterNode() { if (!state.initialized) { return false; } state.showPrimalPoint = true; state.selectedNodeType = "center"; state.selectedConnectorId = null; render(getElements()); return true; } function selectPlacement(criteria = {}) { if (!state.initialized) { return false; } const wallId = normalizeId(criteria.wallId); const connectorId = normalizeId(criteria.connectorId); const edgeId = normalizeEdgeId(criteria.edgeId || criteria.directionId); const hebrewLetterId = normalizeLetterKey(criteria.hebrewLetterId); const signId = normalizeId(criteria.signId || criteria.zodiacSignId); const planetId = normalizeId(criteria.planetId); const pathNo = toFiniteNumber(criteria.pathNo || criteria.kabbalahPathNumber); const trumpNo = toFiniteNumber(criteria.trumpNumber || criteria.tarotTrumpNumber); const nodeType = normalizeId(criteria.nodeType); const centerRequested = nodeType === "center" || Boolean(criteria.center) || Boolean(criteria.primalPoint) || normalizeId(criteria.centerId) === "primal-point"; const edges = getEdges(); const findEdgeBy = (predicate) => edges.find((edge) => predicate(edge)) || null; const findWallForEdge = (edge, preferredWallId) => { const edgeWalls = getEdgeWalls(edge); if (preferredWallId && edgeWalls.includes(preferredWallId)) { return preferredWallId; } return edgeWalls[0] || normalizeId(getWalls()[0]?.id); }; if (connectorId) { return selectConnectorById(connectorId); } if (centerRequested) { return selectCenterNode(); } if (edgeId) { const edge = getEdgeById(edgeId); if (!edge) { return false; } return applyPlacement({ wallId: findWallForEdge(edge, wallId), edgeId }); } if (wallId) { const wall = getWallById(wallId); if (!wall) { return false; } // if an explicit edge id was not provided (or was empty) we treat this // as a request to show the wall/face itself rather than any particular // edge direction. `applyPlacement` only knows how to highlight edges, // so we fall back to selecting the wall directly in that case. this // is the behaviour we want when navigating from a "face" letter like // dalet, where the placement computed by ui-alphabet leaves edgeId // blank. if (!edgeId) { return selectWallById(wallId); } const firstEdge = getEdgesForWall(wallId)[0] || null; return applyPlacement({ wallId, edgeId: firstEdge?.id }); } if (hebrewLetterId) { const byHebrew = findEdgeBy((edge) => getEdgeLetterId(edge) === hebrewLetterId); if (byHebrew) { return applyPlacement({ wallId: findWallForEdge(byHebrew), edgeId: byHebrew.id }); } const byWallFace = getWalls().find((wall) => getWallFaceLetterId(wall) === hebrewLetterId) || null; if (byWallFace) { const byWallFaceId = normalizeId(byWallFace.id); const firstEdge = getEdgesForWall(byWallFaceId)[0] || null; return applyPlacement({ wallId: byWallFaceId, edgeId: firstEdge?.id }); } } if (signId) { const bySign = findEdgeBy((edge) => { const astrology = getEdgePathEntry(edge)?.astrology || {}; return normalizeId(astrology.type) === "zodiac" && normalizeId(astrology.name) === signId; }); if (bySign) { return applyPlacement({ wallId: findWallForEdge(bySign), edgeId: bySign.id }); } } if (pathNo != null) { const byPath = findEdgeBy((edge) => toFiniteNumber(getEdgePathEntry(edge)?.pathNumber) === pathNo); if (byPath) { return applyPlacement({ wallId: findWallForEdge(byPath), edgeId: byPath.id }); } } if (trumpNo != null) { const byTrump = findEdgeBy((edge) => { const tarot = getEdgePathEntry(edge)?.tarot || {}; return toFiniteNumber(tarot.trumpNumber) === trumpNo; }); if (byTrump) { return applyPlacement({ wallId: findWallForEdge(byTrump), edgeId: byTrump.id }); } } if (planetId) { const wall = getWalls().find((entry) => normalizeId(entry?.associations?.planetId) === planetId); if (wall) { const wallIdByPlanet = normalizeId(wall.id); return applyPlacement({ wallId: wallIdByPlanet, edgeId: getEdgesForWall(wallIdByPlanet)[0]?.id }); } } return false; } function selectByHebrewLetterId(hebrewLetterId) { return selectPlacement({ hebrewLetterId }); } function selectBySignId(signId) { return selectPlacement({ signId }); } function selectByPlanetId(planetId) { return selectPlacement({ planetId }); } function selectByPathNo(pathNo) { return selectPlacement({ pathNo }); } window.CubeSectionUi = { ensureCubeSection, selectWallById, selectPlacement, selectByHebrewLetterId, selectBySignId, selectByPlanetId, selectByPathNo, getEdgeDirectionForWall, getEdgeDirectionLabelForWall }; })();