refraction almost completed

This commit is contained in:
2026-03-07 13:38:13 -08:00
parent 3c07a13547
commit d44483de5e
37 changed files with 8506 additions and 7145 deletions

View File

@@ -120,6 +120,8 @@
below: { x: 90, y: 0 }
};
const cubeDetailUi = window.CubeDetailUi || {};
const cubeChassisUi = window.CubeChassisUi || {};
const cubeMathHelpers = window.CubeMathHelpers || {};
function getElements() {
return {
@@ -250,144 +252,48 @@
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.");
}
function normalizeAngle(angle) {
let next = angle;
while (next > 180) {
next -= 360;
}
while (next <= -180) {
next += 360;
}
return next;
return cubeMathUi.normalizeAngle(angle);
}
function setRotation(nextX, nextY) {
state.rotationX = normalizeAngle(nextX);
state.rotationY = normalizeAngle(nextY);
cubeMathUi.setRotation(nextX, nextY);
}
function snapRotationToWall(wallId) {
const target = WALL_FRONT_ROTATIONS[normalizeId(wallId)];
if (!target) {
return;
}
setRotation(target.x, target.y);
cubeMathUi.snapRotationToWall(wallId);
}
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
};
return cubeMathUi.facePoint(quad, u, v);
}
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
};
});
return cubeMathUi.projectVerticesForRotation(rotationX, rotationY);
}
function projectVertices() {
return projectVerticesForRotation(state.rotationX, state.rotationY);
return cubeMathUi.projectVertices();
}
function getEdgeGeometryById(edgeId) {
const canonicalId = normalizeEdgeId(edgeId);
const geometryIndex = EDGE_GEOMETRY_KEYS.indexOf(canonicalId);
if (geometryIndex < 0) {
return null;
}
return EDGE_GEOMETRY[geometryIndex] || null;
return cubeMathUi.getEdgeGeometryById(edgeId);
}
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;
return cubeMathUi.getWallEdgeDirections(wallOrWallId);
}
function getEdgeDirectionForWall(wallId, edgeId) {
const wallKey = normalizeId(wallId);
const edgeKey = normalizeEdgeId(edgeId);
if (!wallKey || !edgeKey) {
return "";
}
const directions = getWallEdgeDirections(wallKey);
return directions.get(edgeKey) || "";
return cubeMathUi.getEdgeDirectionForWall(wallId, edgeId);
}
function getEdgeDirectionLabelForWall(wallId, edgeId) {
return formatDirectionName(getEdgeDirectionForWall(wallId, edgeId));
return cubeMathUi.getEdgeDirectionLabelForWall(wallId, edgeId);
}
function bindRotationControls(elements) {
@@ -444,94 +350,15 @@
}
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;
return cubeMathUi.getHebrewLetterSymbol(hebrewLetterId);
}
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;
return cubeMathUi.getHebrewLetterName(hebrewLetterId);
}
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 "";
return cubeMathUi.getAstrologySymbol(type, name);
}
function getEdgeLetterId(edge) {
@@ -556,16 +383,11 @@
}
function getCenterLetterId(center = null) {
const entry = center || getCubeCenterData();
return normalizeLetterKey(entry?.hebrewLetterId || entry?.associations?.hebrewLetterId || entry?.letter);
return cubeMathUi.getCenterLetterId(center);
}
function getCenterLetterSymbol(center = null) {
const centerLetterId = getCenterLetterId(center);
if (!centerLetterId) {
return "";
}
return getHebrewLetterSymbol(centerLetterId);
return cubeMathUi.getCenterLetterSymbol(center);
}
function getConnectorById(connectorId) {
@@ -591,42 +413,35 @@
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
});
function getEdgeAstrologySymbol(edge) {
const pathEntry = getEdgePathEntry(edge);
const astrology = pathEntry?.astrology || {};
return getAstrologySymbol(astrology.type, astrology.name);
return cubeMathUi.getEdgeAstrologySymbol(edge);
}
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 };
return cubeMathUi.getEdgeMarkerDisplay(edge);
}
function getEdgeLetter(edge) {
const hebrewLetterId = getEdgeLetterId(edge);
if (!hebrewLetterId) {
return "";
}
return getHebrewLetterSymbol(hebrewLetterId);
return cubeMathUi.getEdgeLetter(edge);
}
function getWallTarotCard(wall) {
@@ -700,513 +515,47 @@
}
function renderFaceSvg(containerEl, walls) {
if (!containerEl) {
if (typeof cubeChassisUi.renderFaceSvg !== "function") {
if (containerEl) {
containerEl.replaceChildren();
}
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 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);
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
});
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);
}
function selectEdgeById(edgeId, preferredWallId = "") {