Files
TaroTime/app/ui-cube-chassis.js

550 lines
23 KiB
JavaScript
Raw Normal View History

2026-03-07 13:38:13 -08:00
(function () {
"use strict";
function renderFaceSvg(context) {
const {
state,
containerEl,
walls,
normalizeId,
projectVertices,
FACE_GEOMETRY,
facePoint,
normalizeEdgeId,
getEdges,
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
} = context;
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(context.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;
const cardH = 60;
const wallTarotCard = getWallTarotCard(wall);
const cardImg = document.createElementNS(svgNS, "image");
cardImg.setAttribute("class", "cube-tarot-image cube-face-card");
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", `Open ${wallTarotCard || (wall?.name || wallId)} card image`);
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
cardImg.addEventListener("click", (event) => {
event.stopPropagation();
selectWall();
openTarotCardLightbox(wallTarotCard, cardUrl, `${wall?.name || wallId} wall tarot card`);
});
cardImg.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
selectWall();
openTarotCardLightbox(wallTarotCard, cardUrl, `${wall?.name || wallId} wall tarot card`);
}
});
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);
const selectConnector = () => {
state.selectedNodeType = "connector";
state.selectedConnectorId = connectorId;
render(getElements());
};
if (state.markerDisplayMode === "tarot" && connectorCardUrl) {
const cardW = 18;
const cardH = 27;
const connectorTarotCard = getConnectorTarotCard(connector);
const connectorImg = document.createElementNS(svgNS, "image");
connectorImg.setAttribute("class", "cube-tarot-image cube-connector-card");
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("role", "button");
connectorImg.setAttribute("tabindex", "0");
connectorImg.setAttribute("aria-label", `Open ${connectorTarotCard || connector?.name || "connector"} card image`);
connectorImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
connectorImg.addEventListener("click", (event) => {
event.stopPropagation();
selectConnector();
openTarotCardLightbox(connectorTarotCard, connectorCardUrl, connector?.name || "Mother connector");
});
connectorImg.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
selectConnector();
openTarotCardLightbox(connectorTarotCard, connectorCardUrl, connector?.name || "Mother connector");
}
});
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);
}
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 edgeTarotCard = getEdgeTarotCard(edge);
const cardImg = document.createElementNS(svgNS, "image");
cardImg.setAttribute("class", `cube-tarot-image 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("role", "button");
cardImg.setAttribute("tabindex", "0");
cardImg.setAttribute("aria-label", `Open ${edgeTarotCard || edge?.name || "edge"} card image`);
cardImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
cardImg.addEventListener("click", (event) => {
event.stopPropagation();
selectEdge();
openTarotCardLightbox(edgeTarotCard, edgeCardUrl, edge?.name || "Cube edge");
});
cardImg.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
selectEdge();
openTarotCardLightbox(edgeTarotCard, edgeCardUrl, edge?.name || "Cube edge");
}
});
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 centerTarotCard = getCenterTarotCard(center);
const centerImg = document.createElementNS(svgNS, "image");
centerImg.setAttribute("class", "cube-tarot-image cube-center-card");
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("role", "button");
centerImg.setAttribute("tabindex", "0");
centerImg.setAttribute("aria-label", `Open ${centerTarotCard || "Primal Point"} card image`);
centerImg.setAttribute("preserveAspectRatio", "xMidYMid meet");
centerImg.addEventListener("click", (event) => {
event.stopPropagation();
state.selectedNodeType = "center";
state.selectedConnectorId = null;
render(getElements());
openTarotCardLightbox(centerTarotCard, centerCardUrl, "Primal Point");
});
centerImg.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
state.selectedNodeType = "center";
state.selectedConnectorId = null;
render(getElements());
openTarotCardLightbox(centerTarotCard, centerCardUrl, "Primal Point");
}
});
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);
}
window.CubeChassisUi = { renderFaceSvg };
})();