1057 lines
30 KiB
JavaScript
1057 lines
30 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,
|
|
focusMode: false,
|
|
exportInProgress: false,
|
|
exportFormat: ""
|
|
};
|
|
|
|
const CUBE_EXPORT_FORMATS = {
|
|
webp: {
|
|
mimeType: "image/webp",
|
|
extension: "webp",
|
|
quality: 0.98
|
|
}
|
|
};
|
|
const CUBE_EXPORT_BACKGROUND = "#111118";
|
|
|
|
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 }
|
|
};
|
|
const cubeDetailUi = window.CubeDetailUi || {};
|
|
const cubeChassisUi = window.CubeChassisUi || {};
|
|
const cubeMathHelpers = window.CubeMathHelpers || {};
|
|
const cubeSelectionHelpers = window.CubeSelectionHelpers || {};
|
|
let webpExportSupported = null;
|
|
|
|
function getElements() {
|
|
return {
|
|
cubeSectionEl: document.getElementById("cube-section"),
|
|
cubeLayoutEl: document.getElementById("cube-layout"),
|
|
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"),
|
|
focusToggleEl: document.getElementById("cube-focus-toggle"),
|
|
exportWebpEl: document.getElementById("cube-export-webp"),
|
|
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;
|
|
}
|
|
|
|
if (typeof cubeMathHelpers.createCubeMathHelpers !== "function") {
|
|
throw new Error("CubeMathHelpers.createCubeMathHelpers is unavailable. Ensure app/ui-cube-math.js loads before app/ui-cube.js.");
|
|
}
|
|
|
|
if (typeof cubeSelectionHelpers.createCubeSelectionHelpers !== "function") {
|
|
throw new Error("CubeSelectionHelpers.createCubeSelectionHelpers is unavailable. Ensure app/ui-cube-selection.js loads before app/ui-cube.js.");
|
|
}
|
|
|
|
function normalizeAngle(angle) {
|
|
return cubeMathUi.normalizeAngle(angle);
|
|
}
|
|
|
|
function setRotation(nextX, nextY) {
|
|
cubeMathUi.setRotation(nextX, nextY);
|
|
}
|
|
|
|
function snapRotationToWall(wallId) {
|
|
cubeMathUi.snapRotationToWall(wallId);
|
|
}
|
|
|
|
function facePoint(quad, u, v) {
|
|
return cubeMathUi.facePoint(quad, u, v);
|
|
}
|
|
|
|
function projectVerticesForRotation(rotationX, rotationY) {
|
|
return cubeMathUi.projectVerticesForRotation(rotationX, rotationY);
|
|
}
|
|
|
|
function projectVertices() {
|
|
return cubeMathUi.projectVertices();
|
|
}
|
|
|
|
function getEdgeGeometryById(edgeId) {
|
|
return cubeMathUi.getEdgeGeometryById(edgeId);
|
|
}
|
|
|
|
function getWallEdgeDirections(wallOrWallId) {
|
|
return cubeMathUi.getWallEdgeDirections(wallOrWallId);
|
|
}
|
|
|
|
function getEdgeDirectionForWall(wallId, edgeId) {
|
|
return cubeMathUi.getEdgeDirectionForWall(wallId, edgeId);
|
|
}
|
|
|
|
function getEdgeDirectionLabelForWall(wallId, edgeId) {
|
|
return cubeMathUi.getEdgeDirectionLabelForWall(wallId, edgeId);
|
|
}
|
|
|
|
function rotateAndRender(deltaX, deltaY) {
|
|
setRotation(state.rotationX + deltaX, state.rotationY + deltaY);
|
|
render(getElements());
|
|
}
|
|
|
|
function resetRotationAndRender() {
|
|
setRotation(18, -28);
|
|
render(getElements());
|
|
}
|
|
|
|
function isKeyboardEditableTarget(target) {
|
|
if (!(target instanceof HTMLElement)) {
|
|
return false;
|
|
}
|
|
|
|
if (target.isContentEditable) {
|
|
return true;
|
|
}
|
|
|
|
return ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
|
|
}
|
|
|
|
function isCubeFocusKeyboardModeActive(elements = getElements()) {
|
|
return Boolean(state.focusMode)
|
|
&& elements?.cubeSectionEl instanceof HTMLElement
|
|
&& !elements.cubeSectionEl.hidden;
|
|
}
|
|
|
|
function syncFocusControls(elements) {
|
|
if (elements?.cubeLayoutEl instanceof HTMLElement) {
|
|
elements.cubeLayoutEl.classList.toggle("is-cube-focus", Boolean(state.focusMode));
|
|
}
|
|
|
|
if (elements?.focusToggleEl instanceof HTMLButtonElement) {
|
|
elements.focusToggleEl.setAttribute("aria-pressed", state.focusMode ? "true" : "false");
|
|
elements.focusToggleEl.textContent = state.focusMode ? "Show Full Cube" : "Focus Cube";
|
|
}
|
|
}
|
|
|
|
function isExportFormatSupported(format) {
|
|
const exportFormat = CUBE_EXPORT_FORMATS[format];
|
|
if (!exportFormat) {
|
|
return false;
|
|
}
|
|
|
|
if (format === "webp" && typeof webpExportSupported === "boolean") {
|
|
return webpExportSupported;
|
|
}
|
|
|
|
const probeCanvas = document.createElement("canvas");
|
|
const dataUrl = probeCanvas.toDataURL(exportFormat.mimeType);
|
|
const isSupported = dataUrl.startsWith(`data:${exportFormat.mimeType}`);
|
|
if (format === "webp") {
|
|
webpExportSupported = isSupported;
|
|
}
|
|
return isSupported;
|
|
}
|
|
|
|
function syncExportControls(elements) {
|
|
if (!(elements?.exportWebpEl instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
|
|
const supportsWebp = isExportFormatSupported("webp");
|
|
elements.exportWebpEl.hidden = !supportsWebp;
|
|
elements.exportWebpEl.disabled = Boolean(state.exportInProgress) || !supportsWebp;
|
|
elements.exportWebpEl.textContent = state.exportInProgress && state.exportFormat === "webp"
|
|
? "Exporting..."
|
|
: "Export WebP";
|
|
|
|
if (supportsWebp) {
|
|
elements.exportWebpEl.title = "Download the current cube view as a WebP image.";
|
|
}
|
|
}
|
|
|
|
function copyComputedStyles(sourceEl, targetEl) {
|
|
if (!(sourceEl instanceof Element) || !(targetEl instanceof Element)) {
|
|
return;
|
|
}
|
|
|
|
const computedStyle = window.getComputedStyle(sourceEl);
|
|
Array.from(computedStyle).forEach((propertyName) => {
|
|
targetEl.style.setProperty(
|
|
propertyName,
|
|
computedStyle.getPropertyValue(propertyName),
|
|
computedStyle.getPropertyPriority(propertyName)
|
|
);
|
|
});
|
|
|
|
targetEl.style.setProperty("animation", "none");
|
|
targetEl.style.setProperty("transition", "none");
|
|
}
|
|
|
|
function inlineSvgStyles(sourceNode, targetNode) {
|
|
if (!(sourceNode instanceof Element) || !(targetNode instanceof Element)) {
|
|
return;
|
|
}
|
|
|
|
copyComputedStyles(sourceNode, targetNode);
|
|
|
|
const sourceChildren = Array.from(sourceNode.children);
|
|
const targetChildren = Array.from(targetNode.children);
|
|
const childCount = Math.min(sourceChildren.length, targetChildren.length);
|
|
for (let index = 0; index < childCount; index += 1) {
|
|
inlineSvgStyles(sourceChildren[index], targetChildren[index]);
|
|
}
|
|
}
|
|
|
|
function absolutizeSvgImageLinks(svgEl) {
|
|
if (!(svgEl instanceof SVGSVGElement)) {
|
|
return;
|
|
}
|
|
|
|
svgEl.querySelectorAll("image").forEach((imageEl) => {
|
|
const href = imageEl.getAttribute("href")
|
|
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href");
|
|
if (!href) {
|
|
return;
|
|
}
|
|
|
|
const absoluteHref = new URL(href, document.baseURI).href;
|
|
imageEl.setAttribute("href", absoluteHref);
|
|
imageEl.setAttributeNS("http://www.w3.org/1999/xlink", "href", absoluteHref);
|
|
});
|
|
}
|
|
|
|
function prepareSvgMarkupForExport(svgEl) {
|
|
if (!(svgEl instanceof SVGSVGElement)) {
|
|
throw new Error("Cube view is not ready to export yet.");
|
|
}
|
|
|
|
const bounds = svgEl.getBoundingClientRect();
|
|
const viewBox = svgEl.viewBox?.baseVal || null;
|
|
const width = Math.max(
|
|
240,
|
|
Math.round(bounds.width),
|
|
Number.isFinite(viewBox?.width) ? Math.round(viewBox.width) : 0
|
|
);
|
|
const height = Math.max(
|
|
220,
|
|
Math.round(bounds.height),
|
|
Number.isFinite(viewBox?.height) ? Math.round(viewBox.height) : 0
|
|
);
|
|
|
|
const clone = svgEl.cloneNode(true);
|
|
if (!(clone instanceof SVGSVGElement)) {
|
|
throw new Error("Cube export could not clone the current SVG view.");
|
|
}
|
|
|
|
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
clone.setAttribute("width", String(width));
|
|
clone.setAttribute("height", String(height));
|
|
clone.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
|
|
inlineSvgStyles(svgEl, clone);
|
|
absolutizeSvgImageLinks(clone);
|
|
|
|
const backgroundRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
backgroundRect.setAttribute("x", "0");
|
|
backgroundRect.setAttribute("y", "0");
|
|
backgroundRect.setAttribute("width", "100%");
|
|
backgroundRect.setAttribute("height", "100%");
|
|
backgroundRect.setAttribute("fill", CUBE_EXPORT_BACKGROUND);
|
|
backgroundRect.setAttribute("pointer-events", "none");
|
|
clone.insertBefore(backgroundRect, clone.firstChild);
|
|
|
|
return {
|
|
width,
|
|
height,
|
|
markup: new XMLSerializer().serializeToString(clone)
|
|
};
|
|
}
|
|
|
|
function loadSvgImage(markup) {
|
|
return new Promise((resolve, reject) => {
|
|
const svgBlob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
|
|
const svgUrl = URL.createObjectURL(svgBlob);
|
|
const image = new Image();
|
|
|
|
image.decoding = "async";
|
|
image.onload = () => {
|
|
URL.revokeObjectURL(svgUrl);
|
|
resolve(image);
|
|
};
|
|
image.onerror = () => {
|
|
URL.revokeObjectURL(svgUrl);
|
|
reject(new Error("Cube export renderer could not load the current SVG view."));
|
|
};
|
|
image.src = svgUrl;
|
|
});
|
|
}
|
|
|
|
function canvasToBlobByFormat(canvas, format) {
|
|
const exportFormat = CUBE_EXPORT_FORMATS[format];
|
|
if (!exportFormat) {
|
|
return Promise.reject(new Error("Unsupported export format."));
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
canvas.toBlob((blob) => {
|
|
if (blob) {
|
|
resolve(blob);
|
|
return;
|
|
}
|
|
reject(new Error("Canvas export failed."));
|
|
}, exportFormat.mimeType, exportFormat.quality);
|
|
});
|
|
}
|
|
|
|
async function exportCubeView(format = "webp") {
|
|
const exportFormat = CUBE_EXPORT_FORMATS[format];
|
|
if (!exportFormat || state.exportInProgress) {
|
|
return;
|
|
}
|
|
|
|
const elements = getElements();
|
|
const svgEl = elements.viewContainerEl?.querySelector("svg.cube-svg");
|
|
if (!(svgEl instanceof SVGSVGElement)) {
|
|
window.alert("Cube view is not ready to export yet.");
|
|
return;
|
|
}
|
|
|
|
state.exportInProgress = true;
|
|
state.exportFormat = format;
|
|
syncExportControls(elements);
|
|
|
|
try {
|
|
const { width, height, markup } = prepareSvgMarkupForExport(svgEl);
|
|
const image = await loadSvgImage(markup);
|
|
const scale = Math.max(2, Math.min(4, Number(window.devicePixelRatio) || 1));
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = Math.max(1, Math.ceil(width * scale));
|
|
canvas.height = Math.max(1, Math.ceil(height * scale));
|
|
|
|
const context = canvas.getContext("2d");
|
|
if (!context) {
|
|
throw new Error("Canvas context is unavailable.");
|
|
}
|
|
|
|
context.scale(scale, scale);
|
|
context.imageSmoothingEnabled = true;
|
|
context.imageSmoothingQuality = "high";
|
|
context.fillStyle = CUBE_EXPORT_BACKGROUND;
|
|
context.fillRect(0, 0, width, height);
|
|
context.drawImage(image, 0, 0, width, height);
|
|
|
|
const blob = await canvasToBlobByFormat(canvas, format);
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
const downloadLink = document.createElement("a");
|
|
const stamp = new Date().toISOString().slice(0, 10);
|
|
downloadLink.href = blobUrl;
|
|
downloadLink.download = `cube-of-space-${stamp}.${exportFormat.extension}`;
|
|
document.body.appendChild(downloadLink);
|
|
downloadLink.click();
|
|
downloadLink.remove();
|
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
|
} catch (error) {
|
|
window.alert(error instanceof Error ? error.message : "Unable to export the current cube view.");
|
|
} finally {
|
|
state.exportInProgress = false;
|
|
state.exportFormat = "";
|
|
syncExportControls(getElements());
|
|
}
|
|
}
|
|
|
|
function bindRotationControls(elements) {
|
|
if (state.controlsBound) {
|
|
return;
|
|
}
|
|
|
|
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", resetRotationAndRender);
|
|
elements.focusToggleEl?.addEventListener("click", () => {
|
|
state.focusMode = !state.focusMode;
|
|
syncFocusControls(getElements());
|
|
});
|
|
elements.exportWebpEl?.addEventListener("click", () => {
|
|
void exportCubeView("webp");
|
|
});
|
|
|
|
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());
|
|
});
|
|
}
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) {
|
|
return;
|
|
}
|
|
|
|
if (!isCubeFocusKeyboardModeActive(getElements())) {
|
|
return;
|
|
}
|
|
|
|
if (isKeyboardEditableTarget(event.target)) {
|
|
return;
|
|
}
|
|
|
|
switch (String(event.key || "").toLowerCase()) {
|
|
case "a":
|
|
event.preventDefault();
|
|
rotateAndRender(0, -9);
|
|
break;
|
|
case "d":
|
|
event.preventDefault();
|
|
rotateAndRender(0, 9);
|
|
break;
|
|
case "w":
|
|
event.preventDefault();
|
|
rotateAndRender(-9, 0);
|
|
break;
|
|
case "s":
|
|
event.preventDefault();
|
|
rotateAndRender(9, 0);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
|
|
state.controlsBound = true;
|
|
}
|
|
|
|
function getHebrewLetterSymbol(hebrewLetterId) {
|
|
return cubeMathUi.getHebrewLetterSymbol(hebrewLetterId);
|
|
}
|
|
|
|
function getHebrewLetterName(hebrewLetterId) {
|
|
return cubeMathUi.getHebrewLetterName(hebrewLetterId);
|
|
}
|
|
|
|
function getAstrologySymbol(type, name) {
|
|
return cubeMathUi.getAstrologySymbol(type, name);
|
|
}
|
|
|
|
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) {
|
|
return cubeMathUi.getCenterLetterId(center);
|
|
}
|
|
|
|
function getCenterLetterSymbol(center = null) {
|
|
return cubeMathUi.getCenterLetterSymbol(center);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
const cubeMathUi = cubeMathHelpers.createCubeMathHelpers({
|
|
state,
|
|
CUBE_VERTICES,
|
|
FACE_GEOMETRY,
|
|
EDGE_GEOMETRY,
|
|
EDGE_GEOMETRY_KEYS,
|
|
CUBE_VIEW_CENTER,
|
|
WALL_FRONT_ROTATIONS,
|
|
LOCAL_DIRECTION_VIEW_MAP,
|
|
normalizeId,
|
|
normalizeLetterKey,
|
|
normalizeEdgeId,
|
|
formatDirectionName,
|
|
getEdgesForWall,
|
|
getEdgePathEntry,
|
|
getEdgeLetterId,
|
|
getCubeCenterData
|
|
});
|
|
|
|
const cubeSelectionUi = cubeSelectionHelpers.createCubeSelectionHelpers({
|
|
state,
|
|
normalizeId,
|
|
normalizeEdgeId,
|
|
normalizeLetterKey,
|
|
toFiniteNumber,
|
|
getWalls,
|
|
getWallById,
|
|
getEdges,
|
|
getEdgeById,
|
|
getEdgeWalls,
|
|
getEdgesForWall,
|
|
getEdgeLetterId,
|
|
getWallFaceLetterId,
|
|
getEdgePathEntry,
|
|
getConnectorById,
|
|
snapRotationToWall,
|
|
render,
|
|
getElements
|
|
});
|
|
|
|
function getEdgeAstrologySymbol(edge) {
|
|
return cubeMathUi.getEdgeAstrologySymbol(edge);
|
|
}
|
|
|
|
function getEdgeMarkerDisplay(edge) {
|
|
return cubeMathUi.getEdgeMarkerDisplay(edge);
|
|
}
|
|
|
|
function getEdgeLetter(edge) {
|
|
return cubeMathUi.getEdgeLetter(edge);
|
|
}
|
|
|
|
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 openTarotCardLightbox(cardName, fallbackSrc = "", fallbackLabel = "") {
|
|
const openLightbox = window.TarotUiLightbox?.open;
|
|
if (typeof openLightbox !== "function") {
|
|
return false;
|
|
}
|
|
|
|
const src = toDisplayText(fallbackSrc) || resolveCardImageUrl(cardName);
|
|
if (!src) {
|
|
return false;
|
|
}
|
|
|
|
const label = toDisplayText(cardName) || toDisplayText(fallbackLabel) || "Tarot card";
|
|
openLightbox(src, label);
|
|
return true;
|
|
}
|
|
|
|
function applyPlacement(placement) {
|
|
return cubeSelectionUi.applyPlacement(placement);
|
|
}
|
|
|
|
function toDisplayText(value) {
|
|
return String(value ?? "").trim();
|
|
}
|
|
|
|
function renderFaceSvg(containerEl, walls) {
|
|
if (typeof cubeChassisUi.renderFaceSvg !== "function") {
|
|
if (containerEl) {
|
|
containerEl.replaceChildren();
|
|
}
|
|
return;
|
|
}
|
|
|
|
cubeChassisUi.renderFaceSvg({
|
|
state,
|
|
containerEl,
|
|
walls,
|
|
normalizeId,
|
|
projectVertices,
|
|
FACE_GEOMETRY,
|
|
facePoint,
|
|
normalizeEdgeId,
|
|
getEdges,
|
|
getEdgesForWall,
|
|
EDGE_GEOMETRY,
|
|
EDGE_GEOMETRY_KEYS,
|
|
formatEdgeName,
|
|
getEdgeWalls,
|
|
getElements,
|
|
render,
|
|
snapRotationToWall,
|
|
getWallFaceLetter,
|
|
getWallTarotCard,
|
|
resolveCardImageUrl,
|
|
openTarotCardLightbox,
|
|
MOTHER_CONNECTORS,
|
|
formatDirectionName,
|
|
getConnectorTarotCard,
|
|
getHebrewLetterSymbol,
|
|
toDisplayText,
|
|
CUBE_VIEW_CENTER,
|
|
getEdgeMarkerDisplay,
|
|
getEdgeTarotCard,
|
|
getCubeCenterData,
|
|
getCenterTarotCard,
|
|
getCenterLetterSymbol
|
|
});
|
|
}
|
|
|
|
function selectEdgeById(edgeId, preferredWallId = "") {
|
|
return cubeSelectionUi.selectEdgeById(edgeId, preferredWallId);
|
|
}
|
|
|
|
function renderDetail(elements, walls) {
|
|
if (typeof cubeDetailUi.renderDetail !== "function") {
|
|
if (elements?.detailNameEl) {
|
|
elements.detailNameEl.textContent = "Cube data unavailable";
|
|
}
|
|
if (elements?.detailSubEl) {
|
|
elements.detailSubEl.textContent = "Cube detail renderer missing.";
|
|
}
|
|
if (elements?.detailBodyEl) {
|
|
elements.detailBodyEl.innerHTML = "";
|
|
}
|
|
return;
|
|
}
|
|
|
|
cubeDetailUi.renderDetail({
|
|
state,
|
|
elements,
|
|
walls,
|
|
normalizeId,
|
|
normalizeEdgeId,
|
|
normalizeLetterKey,
|
|
formatDirectionName,
|
|
formatEdgeName,
|
|
toFiniteNumber,
|
|
getWallById,
|
|
getEdgeById,
|
|
getEdges,
|
|
getEdgeWalls,
|
|
getEdgesForWall,
|
|
getWallEdgeDirections,
|
|
getConnectorById,
|
|
getConnectorPathEntry,
|
|
getCubeCenterData,
|
|
getCenterLetterId,
|
|
getCenterLetterSymbol,
|
|
getEdgeLetterId,
|
|
getEdgeLetter,
|
|
getEdgePathEntry,
|
|
getEdgeAstrologySymbol,
|
|
getWallFaceLetterId,
|
|
getWallFaceLetter,
|
|
getHebrewLetterName,
|
|
getHebrewLetterSymbol,
|
|
localDirectionOrder: LOCAL_DIRECTION_ORDER,
|
|
localDirectionRank: LOCAL_DIRECTION_RANK,
|
|
onSelectWall: selectWallById,
|
|
onSelectEdge: selectEdgeById
|
|
});
|
|
}
|
|
|
|
function render(elements) {
|
|
syncFocusControls(elements);
|
|
syncExportControls(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) {
|
|
return cubeSelectionUi.selectWallById(wallId);
|
|
}
|
|
|
|
function selectConnectorById(connectorId) {
|
|
return cubeSelectionUi.selectConnectorById(connectorId);
|
|
}
|
|
|
|
function selectCenterNode() {
|
|
return cubeSelectionUi.selectCenterNode();
|
|
}
|
|
|
|
function selectPlacement(criteria = {}) {
|
|
return cubeSelectionUi.selectPlacement(criteria);
|
|
}
|
|
|
|
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
|
|
};
|
|
})();
|