Files
TaroTime/app/ui-cube.js
2026-03-07 01:09:00 -08:00

1902 lines
63 KiB
JavaScript

(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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function toDetailValueMarkup(value) {
const text = toDisplayText(value);
return text ? escapeHtml(text) : '<span class="cube-missing-value">!</span>';
}
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 = `
<dl class="alpha-dl">
<dt>Name</dt><dd>${toDetailValueMarkup(center?.name)}</dd>
<dt>Letter</dt><dd>${toDetailValueMarkup(centerLetterText)}</dd>
<dt>Element</dt><dd>${toDetailValueMarkup(center?.element)}</dd>
</dl>
`;
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 = "<strong>Correspondence Links</strong>";
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 = `
<dl class="alpha-dl">
<dt>Letter</dt><dd>${toDetailValueMarkup(letterText)}</dd>
<dt>From</dt><dd>${toDetailValueMarkup(fromWall?.name || formatDirectionName(fromWallId))}</dd>
<dt>To</dt><dd>${toDetailValueMarkup(toWall?.name || formatDirectionName(toWallId))}</dd>
<dt>Tarot</dt><dd>${toDetailValueMarkup(tarotCard || (tarotTrumpNumber != null ? `Trump ${tarotTrumpNumber}` : ""))}</dd>
</dl>
`;
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 = "<strong>Correspondence Links</strong>";
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 = `
<dl class="alpha-dl">
<dt>Direction</dt><dd>${toDetailValueMarkup(edgeName)}</dd>
<dt>Edge</dt><dd>${toDetailValueMarkup(edgeWalls)}</dd>
<dt>Letter</dt><dd>${toDetailValueMarkup(edgeLetter)}</dd>
<dt>Astrology</dt><dd>${toDetailValueMarkup(astrologyText)}</dd>
<dt>Tarot</dt><dd>${toDetailValueMarkup(tarotCard)}</dd>
</dl>
`;
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 = `
<dl class="alpha-dl">
<dt>Opposite</dt><dd>${toDetailValueMarkup(wall.opposite)}</dd>
<dt>Face Letter</dt><dd>${toDetailValueMarkup(wallFaceLetterText)}</dd>
<dt>Element</dt><dd>${toDetailValueMarkup(wall.element)}</dd>
<dt>Planet</dt><dd>${toDetailValueMarkup(wall.planet)}</dd>
<dt>Archangel</dt><dd>${toDetailValueMarkup(wall.archangel)}</dd>
</dl>
`;
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 = "<strong>Correspondence Links</strong>";
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 = "<strong>Wall Edges</strong>";
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
};
})();