Files
TaroTime/app/ui-kabbalah.js
2026-03-12 21:01:32 -07:00

824 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
"use strict";
// ─── SVG tree layout constants ──────────────────────────────────────────────
const NS = "http://www.w3.org/2000/svg";
const R = 11; // sephira circle radius
// Standard Hermetic GD Tree of Life positions in a 240×470 viewBox
const NODE_POS = {
1: [120, 30], // Kether — crown of middle pillar
2: [200, 88], // Chokmah — right pillar
3: [40, 88], // Binah — left pillar
4: [200, 213], // Chesed — right pillar
5: [40, 213], // Geburah — left pillar
6: [120, 273], // Tiphareth — middle pillar
7: [200, 343], // Netzach — right pillar
8: [40, 343], // Hod — left pillar
9: [120, 398], // Yesod — middle pillar
10: [120, 448], // Malkuth — bottom of middle pillar
};
// King-scale fill colours
const SEPH_FILL = {
1: "#e8e8e8", // Kether — white brilliance
2: "#87ceeb", // Chokmah — soft sky blue
3: "#7d2b5a", // Binah — dark crimson
4: "#1a56e0", // Chesed — deep blue
5: "#d44014", // Geburah — scarlet
6: "#d4a017", // Tiphareth — gold
7: "#22aa60", // Netzach — emerald
8: "#cc5500", // Hod — orange
9: "#6030c0", // Yesod — violet
10: "#c8b000", // Malkuth — citrine / amber
};
// Nodes with light-ish fills get dark number text; others get white
const DARK_TEXT = new Set([1, 2, 6, 10]);
// Da'at phantom sephira drawn as a dashed circle, not clickable
const DAAT = [120, 148];
const PATH_MARKER_SCALE = 1.33;
const PATH_LABEL_RADIUS = 9 * PATH_MARKER_SCALE;
const PATH_LABEL_FONT_SIZE = 8.8 * PATH_MARKER_SCALE;
const PATH_TAROT_WIDTH = 16 * PATH_MARKER_SCALE;
const PATH_TAROT_HEIGHT = 24 * PATH_MARKER_SCALE;
const PATH_LABEL_OFFSET_WITH_TAROT = 11 * PATH_MARKER_SCALE;
const PATH_TAROT_OFFSET_WITH_LABEL = 1 * PATH_MARKER_SCALE;
const PATH_TAROT_OFFSET_NO_LABEL = 12 * PATH_MARKER_SCALE;
// ─── state ──────────────────────────────────────────────────────────────────
const state = {
initialized: false,
tree: null,
godsData: {},
hebrewLetterIdByToken: {},
fourWorldLayers: [],
showPathLetters: true,
showPathNumbers: true,
showPathTarotCards: false,
selectedSephiraNumber: null,
selectedPathNumber: null,
exportInProgress: false,
exportFormat: ""
};
const TREE_EXPORT_FORMATS = {
webp: {
mimeType: "image/webp",
extension: "webp",
quality: 0.98
}
};
const TREE_EXPORT_BACKGROUND = "#02030a";
const kabbalahDetailUi = window.KabbalahDetailUi || {};
const kabbalahViewsUi = window.KabbalahViewsUi || {};
let webpExportSupported = null;
if (
typeof kabbalahViewsUi.renderTree !== "function"
|| typeof kabbalahViewsUi.renderRoseCross !== "function"
) {
throw new Error("KabbalahViewsUi module must load before ui-kabbalah.js");
}
const PLANET_NAME_TO_ID = {
saturn: "saturn",
jupiter: "jupiter",
mars: "mars",
sol: "sol",
sun: "sol",
venus: "venus",
mercury: "mercury",
luna: "luna",
moon: "luna"
};
const ZODIAC_NAME_TO_ID = {
aries: "aries",
taurus: "taurus",
gemini: "gemini",
cancer: "cancer",
leo: "leo",
virgo: "virgo",
libra: "libra",
scorpio: "scorpio",
sagittarius: "sagittarius",
capricorn: "capricorn",
aquarius: "aquarius",
pisces: "pisces"
};
const HEBREW_LETTER_ALIASES = {
aleph: "alef",
alef: "alef",
beth: "bet",
bet: "bet",
gimel: "gimel",
daleth: "dalet",
dalet: "dalet",
he: "he",
vav: "vav",
zayin: "zayin",
cheth: "het",
chet: "het",
het: "het",
teth: "tet",
tet: "tet",
yod: "yod",
kaph: "kaf",
kaf: "kaf",
lamed: "lamed",
mem: "mem",
nun: "nun",
samekh: "samekh",
ayin: "ayin",
pe: "pe",
tzaddi: "tsadi",
tzadi: "tsadi",
tsadi: "tsadi",
qoph: "qof",
qof: "qof",
resh: "resh",
shin: "shin",
tav: "tav"
};
const DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS = [
{
slot: "Yod",
letterChar: "י",
hebrewToken: "yod",
world: "Atziluth",
worldLayer: "Archetypal World (Gods Will)",
worldDescription: "World of gods or specific facets or divine qualities.",
soulLayer: "Chiah",
soulTitle: "Life Force",
soulDescription: "The Chiah is the Life Force itself and our true identity as reflection of Supreme Consciousness."
},
{
slot: "Heh",
letterChar: "ה",
hebrewToken: "he",
world: "Briah",
worldLayer: "Creative World (Gods Love)",
worldDescription: "World of archangels, executors of divine qualities.",
soulLayer: "Neshamah",
soulTitle: "Soul-Intuition",
soulDescription: "The Neshamah is the part of our soul that transcends the thinking process."
},
{
slot: "Vav",
letterChar: "ו",
hebrewToken: "vav",
world: "Yetzirah",
worldLayer: "Formative World (Gods Mind)",
worldDescription: "World of angels who work under archangelic direction.",
soulLayer: "Ruach",
soulTitle: "Intellect",
soulDescription: "The Ruach is the thinking mind that often dominates attention and identity."
},
{
slot: "Heh (final)",
letterChar: "ה",
hebrewToken: "he",
world: "Assiah",
worldLayer: "Material World (Gods Creation)",
worldDescription: "World of spirits that infuse matter and energy through specialized duties.",
soulLayer: "Nephesh",
soulTitle: "Animal Soul",
soulDescription: "The Nephesh is instinctive consciousness expressed through appetite, emotion, sex drive, and survival."
}
];
function titleCase(value) {
return String(value || "")
.split(/[\s-_]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
function normalizeSoulId(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z]/g, "");
}
function buildFourWorldLayersFromDataset(magickDataset) {
const worlds = magickDataset?.grouped?.kabbalah?.fourWorlds;
const souls = magickDataset?.grouped?.kabbalah?.souls;
if (!worlds || typeof worlds !== "object") {
return [...DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS];
}
const worldOrder = ["atzilut", "briah", "yetzirah", "assiah"];
const soulAliases = {
chiah: "chaya",
chaya: "chaya",
neshamah: "neshama",
neshama: "neshama",
ruach: "ruach",
nephesh: "nephesh"
};
return worldOrder.map((worldId, index) => {
const fallback = DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS[index] || {};
const worldEntry = worlds?.[worldId] || null;
if (!worldEntry || typeof worldEntry !== "object") {
return fallback;
}
const tetragrammaton = worldEntry?.tetragrammaton && typeof worldEntry.tetragrammaton === "object"
? worldEntry.tetragrammaton
: {};
const rawSoulId = normalizeSoulId(worldEntry?.soulId);
const soulId = soulAliases[rawSoulId] || rawSoulId;
const soulEntry = souls?.[soulId] && typeof souls[soulId] === "object"
? souls[soulId]
: null;
const soulLayer = soulEntry?.name?.roman || fallback.soulLayer || titleCase(rawSoulId || soulId);
const soulTitle = soulEntry?.title?.en || fallback.soulTitle || titleCase(soulEntry?.name?.en || "");
const soulDescription = soulEntry?.desc?.en || fallback.soulDescription || "";
return {
slot: tetragrammaton?.isFinal
? `${String(tetragrammaton?.slot || fallback.slot || "Heh")} (final)`
: String(tetragrammaton?.slot || fallback.slot || ""),
letterChar: String(tetragrammaton?.letterChar || fallback.letterChar || ""),
hebrewToken: String(tetragrammaton?.hebrewLetterId || fallback.hebrewToken || "").toLowerCase(),
world: String(worldEntry?.name?.roman || fallback.world || titleCase(worldEntry?.id || worldId)),
worldLayer: String(worldEntry?.worldLayer?.en || fallback.worldLayer || worldEntry?.desc?.en || ""),
worldDescription: String(worldEntry?.worldDescription?.en || fallback.worldDescription || ""),
soulLayer: String(soulLayer || ""),
soulTitle: String(soulTitle || ""),
soulDescription: String(soulDescription || "")
};
}).filter(Boolean);
}
// ─── element references ─────────────────────────────────────────────────────
function getElements() {
return {
treeContainerEl: document.getElementById("kab-tree-container"),
detailNameEl: document.getElementById("kab-detail-name"),
detailSubEl: document.getElementById("kab-detail-sub"),
detailBodyEl: document.getElementById("kab-detail-body"),
pathLetterToggleEl: document.getElementById("kab-path-letter-toggle"),
pathNumberToggleEl: document.getElementById("kab-path-number-toggle"),
pathTarotToggleEl: document.getElementById("kab-path-tarot-toggle"),
treeExportWebpEl: document.getElementById("kab-tree-export-webp"),
roseCrossContainerEl: document.getElementById("kab-rose-cross-container"),
roseDetailNameEl: document.getElementById("kab-rose-detail-name"),
roseDetailSubEl: document.getElementById("kab-rose-detail-sub"),
roseDetailBodyEl: document.getElementById("kab-rose-detail-body"),
};
}
function getRoseDetailElements(elements) {
if (!elements) {
return null;
}
return {
detailNameEl: elements.roseDetailNameEl,
detailSubEl: elements.roseDetailSubEl,
detailBodyEl: elements.roseDetailBodyEl
};
}
function normalizeText(value) {
return String(value || "").trim().toLowerCase();
}
function normalizeLetterToken(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z]/g, "");
}
function buildHebrewLetterLookup(magickDataset) {
const letters = magickDataset?.grouped?.hebrewLetters;
const lookup = {};
if (!letters || typeof letters !== "object") {
return lookup;
}
Object.entries(letters).forEach(([letterId, entry]) => {
const entryId = String(entry?.id || letterId || "");
const idToken = normalizeLetterToken(letterId);
const canonicalIdToken = HEBREW_LETTER_ALIASES[idToken] || idToken;
if (canonicalIdToken && !lookup[canonicalIdToken]) {
lookup[canonicalIdToken] = entryId;
}
const nameToken = normalizeLetterToken(entry?.letter?.name);
const canonicalNameToken = HEBREW_LETTER_ALIASES[nameToken] || nameToken;
if (canonicalNameToken && !lookup[canonicalNameToken]) {
lookup[canonicalNameToken] = entryId;
}
});
return lookup;
}
function resolveHebrewLetterId(value) {
const token = normalizeLetterToken(value);
if (!token) return null;
const canonical = HEBREW_LETTER_ALIASES[token] || token;
return state.hebrewLetterIdByToken[canonical] || state.hebrewLetterIdByToken[token] || null;
}
function resolvePlanetId(value) {
const text = normalizeText(value);
if (!text) return null;
for (const [key, planetId] of Object.entries(PLANET_NAME_TO_ID)) {
if (text === key || text.includes(key)) {
return planetId;
}
}
return null;
}
function resolveZodiacId(value) {
const text = normalizeText(value);
if (!text) return null;
for (const [name, zodiacId] of Object.entries(ZODIAC_NAME_TO_ID)) {
if (text === name || text.includes(name)) {
return zodiacId;
}
}
return null;
}
function findPathByHebrewToken(tree, hebrewToken) {
const canonicalToken = HEBREW_LETTER_ALIASES[normalizeLetterToken(hebrewToken)] || normalizeLetterToken(hebrewToken);
if (!canonicalToken) {
return null;
}
const paths = Array.isArray(tree?.paths) ? tree.paths : [];
return paths.find((path) => {
const letterToken = normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char);
const canonicalLetterToken = HEBREW_LETTER_ALIASES[letterToken] || letterToken;
return canonicalLetterToken === canonicalToken;
}) || null;
}
function getDetailRenderContext(tree, elements, extra = {}) {
return {
tree,
elements,
godsData: state.godsData,
fourWorldLayers: state.fourWorldLayers,
resolvePlanetId,
resolveZodiacId,
resolveHebrewLetterId,
findPathByHebrewToken,
...extra
};
}
function clearHighlights() {
document.querySelectorAll(".kab-node, .kab-node-glow")
.forEach(el => el.classList.remove("kab-node-active"));
document.querySelectorAll(".kab-path-hit, .kab-path-line, .kab-path-lbl, .kab-path-tarot, .kab-rose-petal")
.forEach(el => el.classList.remove("kab-path-active"));
}
function renderSephiraDetail(seph, tree, elements) {
state.selectedSephiraNumber = Number(seph?.number);
state.selectedPathNumber = null;
clearHighlights();
document.querySelectorAll(`.kab-node[data-sephira="${seph.number}"], .kab-node-glow[data-sephira="${seph.number}"]`)
.forEach(el => el.classList.add("kab-node-active"));
if (typeof kabbalahDetailUi.renderSephiraDetail === "function") {
kabbalahDetailUi.renderSephiraDetail(getDetailRenderContext(tree, elements, {
seph,
onPathSelect: (path) => renderPathDetail(path, tree, elements)
}));
}
}
function renderPathDetail(path, tree, elements) {
state.selectedPathNumber = Number(path?.pathNumber);
state.selectedSephiraNumber = null;
clearHighlights();
document.querySelectorAll(`[data-path="${path.pathNumber}"]`)
.forEach(el => el.classList.add("kab-path-active"));
if (typeof kabbalahDetailUi.renderPathDetail === "function") {
kabbalahDetailUi.renderPathDetail(getDetailRenderContext(tree, elements, {
path,
activeHebrewToken: normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char || "")
}));
}
}
function renderRoseLandingIntro(roseElements) {
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
kabbalahDetailUi.renderRoseLandingIntro(roseElements);
}
}
function getViewRenderContext(elements) {
return {
state,
tree: state.tree,
elements,
getRoseDetailElements,
renderSephiraDetail,
renderPathDetail,
NS,
R,
NODE_POS,
SEPH_FILL,
DARK_TEXT,
DAAT,
PATH_MARKER_SCALE,
PATH_LABEL_RADIUS,
PATH_LABEL_FONT_SIZE,
PATH_TAROT_WIDTH,
PATH_TAROT_HEIGHT,
PATH_LABEL_OFFSET_WITH_TAROT,
PATH_TAROT_OFFSET_WITH_LABEL,
PATH_TAROT_OFFSET_NO_LABEL
};
}
function renderRoseCurrentSelection(elements) {
if (!state.tree) {
return;
}
const roseElements = getRoseDetailElements(elements);
if (!roseElements?.detailBodyEl) {
return;
}
if (Number.isFinite(Number(state.selectedPathNumber))) {
const selectedPath = state.tree.paths.find((entry) => entry.pathNumber === Number(state.selectedPathNumber));
if (selectedPath) {
renderPathDetail(selectedPath, state.tree, roseElements);
return;
}
}
renderRoseLandingIntro(roseElements);
}
function renderRoseCross(elements) {
kabbalahViewsUi.renderRoseCross(getViewRenderContext(elements));
}
function renderTree(elements) {
kabbalahViewsUi.renderTree(getViewRenderContext(elements));
}
function isExportFormatSupported(format) {
const exportFormat = TREE_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?.treeExportWebpEl instanceof HTMLButtonElement)) {
return;
}
const supportsWebp = isExportFormatSupported("webp");
elements.treeExportWebpEl.hidden = !supportsWebp;
elements.treeExportWebpEl.disabled = Boolean(state.exportInProgress) || !supportsWebp;
elements.treeExportWebpEl.textContent = state.exportInProgress && state.exportFormat === "webp"
? "Exporting..."
: "Export WebP";
if (supportsWebp) {
elements.treeExportWebpEl.title = "Download the current Tree of Life 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("Tree 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(
470,
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("Tree 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(NS, "rect");
backgroundRect.setAttribute("x", "0");
backgroundRect.setAttribute("y", "0");
backgroundRect.setAttribute("width", "100%");
backgroundRect.setAttribute("height", "100%");
backgroundRect.setAttribute("fill", TREE_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("Tree export renderer could not load the current SVG view."));
};
image.src = svgUrl;
});
}
function canvasToBlobByFormat(canvas, format) {
const exportFormat = TREE_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 exportTreeView(format = "webp") {
const exportFormat = TREE_EXPORT_FORMATS[format];
if (!exportFormat || state.exportInProgress) {
return;
}
const elements = getElements();
const svgEl = elements.treeContainerEl?.querySelector("svg.kab-svg");
if (!(svgEl instanceof SVGSVGElement)) {
window.alert("Tree 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 = TREE_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 = `tree-of-life-${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 Tree of Life view.");
} finally {
state.exportInProgress = false;
state.exportFormat = "";
syncExportControls(getElements());
}
}
function renderCurrentSelection(elements) {
if (!state.tree) {
return;
}
if (Number.isFinite(Number(state.selectedPathNumber))) {
const selectedPath = state.tree.paths.find((entry) => entry.pathNumber === Number(state.selectedPathNumber));
if (selectedPath) {
renderPathDetail(selectedPath, state.tree, elements);
return;
}
}
if (Number.isFinite(Number(state.selectedSephiraNumber))) {
const selectedSephira = state.tree.sephiroth.find((entry) => entry.number === Number(state.selectedSephiraNumber));
if (selectedSephira) {
renderSephiraDetail(selectedSephira, state.tree, elements);
return;
}
}
renderSephiraDetail(state.tree.sephiroth[0], state.tree, elements);
}
// ─── initialise section ──────────────────────────────────────────────────────
function init(magickDataset, elements) {
const tree = magickDataset?.grouped?.kabbalah?.["kabbalah-tree"];
if (!tree) {
if (elements.detailNameEl) {
elements.detailNameEl.textContent = "Kabbalah data unavailable";
elements.detailSubEl.textContent = "Could not load kabbalah-tree.json";
}
return;
}
state.tree = tree;
state.godsData = magickDataset?.grouped?.["gods"]?.byPath || {};
state.hebrewLetterIdByToken = buildHebrewLetterLookup(magickDataset);
state.fourWorldLayers = buildFourWorldLayersFromDataset(magickDataset);
const bindPathDisplayToggle = (toggleEl, stateKey) => {
if (!toggleEl) {
return;
}
toggleEl.checked = Boolean(state[stateKey]);
if (toggleEl.dataset.bound) {
return;
}
toggleEl.addEventListener("change", () => {
state[stateKey] = Boolean(toggleEl.checked);
renderTree(elements);
renderCurrentSelection(elements);
});
toggleEl.dataset.bound = "true";
};
bindPathDisplayToggle(elements.pathLetterToggleEl, "showPathLetters");
bindPathDisplayToggle(elements.pathNumberToggleEl, "showPathNumbers");
bindPathDisplayToggle(elements.pathTarotToggleEl, "showPathTarotCards");
syncExportControls(elements);
if (elements.treeExportWebpEl && !elements.treeExportWebpEl.dataset.bound) {
elements.treeExportWebpEl.addEventListener("click", () => {
void exportTreeView("webp");
});
elements.treeExportWebpEl.dataset.bound = "true";
}
renderTree(elements);
renderCurrentSelection(elements);
renderRoseCross(elements);
renderRoseCurrentSelection(elements);
}
function selectPathByNumber(pathNumber) {
if (!state.initialized || !state.tree) return;
const el = getElements();
const path = state.tree.paths.find(p => p.pathNumber === pathNumber);
if (path) renderPathDetail(path, state.tree, el);
}
function selectSephiraByNumber(n) {
if (!state.initialized || !state.tree) return;
const el = getElements();
const seph = state.tree.sephiroth.find(s => s.number === n);
if (seph) renderSephiraDetail(seph, state.tree, el);
}
// select sephirah (1-10) or path (11+) by a single number
function selectNode(n) {
if (n >= 1 && n <= 10) selectSephiraByNumber(n);
else selectPathByNumber(n);
}
// ─── public API ────────────────────────────────────────────────────────
function ensureKabbalahSection(magickDataset) {
if (state.initialized) return;
state.initialized = true;
const elements = getElements();
init(magickDataset, elements);
}
window.KabbalahSectionUi = { ensureKabbalahSection, selectPathByNumber, selectSephiraByNumber, selectNode };
})();