refraction almost completed
This commit is contained in:
811
app/ui-cube.js
811
app/ui-cube.js
@@ -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 = "") {
|
||||
|
||||
Reference in New Issue
Block a user