1902 lines
63 KiB
JavaScript
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, "&")
|
||
|
|
.replace(/</g, "<")
|
||
|
|
.replace(/>/g, ">")
|
||
|
|
.replace(/\"/g, """)
|
||
|
|
.replace(/'/g, "'");
|
||
|
|
}
|
||
|
|
|
||
|
|
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
|
||
|
|
};
|
||
|
|
})();
|