Added Point::is_visible() to explosion.rs: - A point at geom_dim d is visible iff coords[d..] are all singular - Matches homotopy.io's visibility filter (mesh.rs:111-115) Updated render_braiding.rs: - Filter to visible elements only (7 of 23 points for half_braid) - Compute layout coordinates: x=time, y=height, z=depth - Wires spread at z = [-1, 0, 1], vertices at z = [-0.5, 0.5] - No volumes in output (not rendered) Visible elements for half_braid: - 2 vertices: (s0,s0,s0), (s1,s0,s0) - 3 wires: (r0,s0,s0), (r1,s0,s0), (r2,s0,s0) - 2 surfaces: (r0,r0,s0), (r0,r1,s0) Updated web/zigzag-renderer.jsx with new geometry data. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
689 lines
22 KiB
JavaScript
689 lines
22 KiB
JavaScript
// Three.js renderer for zigzag-engine half_braid geometry
|
||
// Renders VISIBLE elements only from explosion poset
|
||
// Visibility: point at geom_dim d is visible iff coords[d..] are all singular
|
||
|
||
const GEOMETRY_DATA = {
|
||
"metadata": {
|
||
"source": "half_braid.json",
|
||
"dimension": 3,
|
||
"total_points": 23,
|
||
"visible_points": 7,
|
||
"total_covers": 35
|
||
},
|
||
"vertices": [
|
||
{ "id": 21, "label": "vertex_0", "point": "s0,s0,s0", "coords": [0.0, 0.0, -0.5] },
|
||
{ "id": 22, "label": "vertex_1", "point": "s1,s0,s0", "coords": [0.0, 0.0, 0.5] }
|
||
],
|
||
"wires": [
|
||
{ "id": 18, "label": "wire_0", "point": "r0,s0,s0", "coords": [0.0, 0.0, -1.0],
|
||
"endpoints": [21, 22], "endpoint_coords": [[0.0, 0.0, -0.5], [0.0, 0.0, 0.5]] },
|
||
{ "id": 19, "label": "wire_1", "point": "r1,s0,s0", "coords": [0.0, 0.0, 0.0],
|
||
"endpoints": [21, 22], "endpoint_coords": [[0.0, 0.0, -0.5], [0.0, 0.0, 0.5]] },
|
||
{ "id": 20, "label": "wire_2", "point": "r2,s0,s0", "coords": [0.0, 0.0, 1.0],
|
||
"endpoints": [21, 22], "endpoint_coords": [[0.0, 0.0, -0.5], [0.0, 0.0, 0.5]] }
|
||
],
|
||
"surfaces": [
|
||
{ "id": 16, "label": "surface_0", "point": "r0,r0,s0", "coords": [0.0, -1.0, -1.0], "boundary_wires": [18] },
|
||
{ "id": 17, "label": "surface_1", "point": "r0,r1,s0", "coords": [0.0, 0.0, -1.0], "boundary_wires": [18] }
|
||
]
|
||
};
|
||
|
||
// Coordinate mapping: coords are already [x, y, z] layout coordinates
|
||
// Scale for better visualization
|
||
const SCALE = 2.0;
|
||
function mapCoords(coords) {
|
||
return [coords[0] * SCALE, coords[1] * SCALE, coords[2] * SCALE];
|
||
}
|
||
|
||
// Wire colors by group
|
||
const WIRE_COLORS = {
|
||
input: 0x4fc3f7, // blue - r0 in third coord
|
||
crossing: 0xe040fb, // purple - s0 in third coord
|
||
output: 0x66bb6a, // green - r1 in third coord
|
||
selfloop: 0xff7043 // orange - self-loops
|
||
};
|
||
|
||
function getWireGroup(wire) {
|
||
if (wire.endpoints[0] === wire.endpoints[1]) return 'selfloop';
|
||
const point = wire.point;
|
||
const thirdCoord = point.split(',')[2];
|
||
if (thirdCoord === 'r0') return 'input';
|
||
if (thirdCoord === 's0') return 'crossing';
|
||
if (thirdCoord === 'r1') return 'output';
|
||
return 'crossing';
|
||
}
|
||
|
||
// Variational elastic curve solver
|
||
// Minimizes E = τ·Σ|Δp|² + β·Σ|Δ²p|² + κ·|p_mid - waypoint|²
|
||
function solveElasticCurve(p0, p1, waypoint, tau, beta, kappa, resolution) {
|
||
const n = resolution; // interior points
|
||
const total = n + 2; // including endpoints
|
||
|
||
// Build matrix A = τ·DᵀD + β·D²ᵀD² + κ·(waypoint spring at midpoint)
|
||
// DᵀD tridiagonal: main=2, off=-1
|
||
// D²ᵀD² pentadiagonal: [1, -4, 6, -4, 1]
|
||
|
||
// We solve for interior points only (indices 1..n)
|
||
// Matrix is n×n
|
||
|
||
const A = [];
|
||
const bx = new Array(n).fill(0);
|
||
const by = new Array(n).fill(0);
|
||
const bz = new Array(n).fill(0);
|
||
|
||
for (let i = 0; i < n; i++) {
|
||
A[i] = new Array(n).fill(0);
|
||
}
|
||
|
||
// Build DᵀD (tension term)
|
||
for (let i = 0; i < n; i++) {
|
||
A[i][i] += 2 * tau;
|
||
if (i > 0) A[i][i-1] += -tau;
|
||
if (i < n-1) A[i][i+1] += -tau;
|
||
}
|
||
|
||
// Boundary contributions to RHS for tension term
|
||
bx[0] += tau * p0[0];
|
||
by[0] += tau * p0[1];
|
||
bz[0] += tau * p0[2];
|
||
bx[n-1] += tau * p1[0];
|
||
by[n-1] += tau * p1[1];
|
||
bz[n-1] += tau * p1[2];
|
||
|
||
// Build D²ᵀD² (bending term) - pentadiagonal
|
||
// For interior point i, D²p[i] = p[i-2] - 4p[i-1] + 6p[i] - 4p[i+1] + p[i+2]
|
||
for (let i = 0; i < n; i++) {
|
||
A[i][i] += 6 * beta;
|
||
if (i > 0) A[i][i-1] += -4 * beta;
|
||
if (i > 1) A[i][i-2] += beta;
|
||
if (i < n-1) A[i][i+1] += -4 * beta;
|
||
if (i < n-2) A[i][i+2] += beta;
|
||
}
|
||
|
||
// Boundary contributions for bending term
|
||
// At i=0: needs p[-1]=p0, p[-2] (extrapolate as p0)
|
||
// At i=1: needs p[-1]=p0
|
||
// At i=n-2: needs p[n]=p1
|
||
// At i=n-1: needs p[n]=p1, p[n+1] (extrapolate as p1)
|
||
|
||
// i=0 contributions from p0 (at index -1) and extrapolated p0 (at index -2)
|
||
bx[0] += beta * (4 * p0[0] - p0[0]); // -4*p[-1] + p[-2], but these go to RHS with sign flip
|
||
by[0] += beta * (4 * p0[1] - p0[1]);
|
||
bz[0] += beta * (4 * p0[2] - p0[2]);
|
||
|
||
if (n > 1) {
|
||
bx[1] += beta * p0[0];
|
||
by[1] += beta * p0[1];
|
||
bz[1] += beta * p0[2];
|
||
}
|
||
|
||
// i=n-1 contributions from p1 (at index n) and extrapolated p1 (at index n+1)
|
||
bx[n-1] += beta * (4 * p1[0] - p1[0]);
|
||
by[n-1] += beta * (4 * p1[1] - p1[1]);
|
||
bz[n-1] += beta * (4 * p1[2] - p1[2]);
|
||
|
||
if (n > 1) {
|
||
bx[n-2] += beta * p1[0];
|
||
by[n-2] += beta * p1[1];
|
||
bz[n-2] += beta * p1[2];
|
||
}
|
||
|
||
// Waypoint spring at midpoint
|
||
const midIdx = Math.floor(n / 2);
|
||
A[midIdx][midIdx] += kappa;
|
||
bx[midIdx] += kappa * waypoint[0];
|
||
by[midIdx] += kappa * waypoint[1];
|
||
bz[midIdx] += kappa * waypoint[2];
|
||
|
||
// Solve using Gaussian elimination with partial pivoting
|
||
function solveTridiagonal(A, b) {
|
||
const n = b.length;
|
||
const x = new Array(n);
|
||
const Ac = A.map(row => [...row]);
|
||
const bc = [...b];
|
||
|
||
// Forward elimination
|
||
for (let k = 0; k < n-1; k++) {
|
||
// Find pivot
|
||
let maxIdx = k;
|
||
for (let i = k+1; i < n; i++) {
|
||
if (Math.abs(Ac[i][k]) > Math.abs(Ac[maxIdx][k])) maxIdx = i;
|
||
}
|
||
// Swap rows
|
||
[Ac[k], Ac[maxIdx]] = [Ac[maxIdx], Ac[k]];
|
||
[bc[k], bc[maxIdx]] = [bc[maxIdx], bc[k]];
|
||
|
||
if (Math.abs(Ac[k][k]) < 1e-10) continue;
|
||
|
||
for (let i = k+1; i < n; i++) {
|
||
const factor = Ac[i][k] / Ac[k][k];
|
||
for (let j = k; j < n; j++) {
|
||
Ac[i][j] -= factor * Ac[k][j];
|
||
}
|
||
bc[i] -= factor * bc[k];
|
||
}
|
||
}
|
||
|
||
// Back substitution
|
||
for (let i = n-1; i >= 0; i--) {
|
||
let sum = bc[i];
|
||
for (let j = i+1; j < n; j++) {
|
||
sum -= Ac[i][j] * x[j];
|
||
}
|
||
x[i] = Math.abs(Ac[i][i]) > 1e-10 ? sum / Ac[i][i] : 0;
|
||
}
|
||
|
||
return x;
|
||
}
|
||
|
||
const solX = solveTridiagonal(A, bx);
|
||
const solY = solveTridiagonal(A, by);
|
||
const solZ = solveTridiagonal(A, bz);
|
||
|
||
// Build full curve with endpoints
|
||
const curve = [p0];
|
||
for (let i = 0; i < n; i++) {
|
||
curve.push([solX[i], solY[i], solZ[i]]);
|
||
}
|
||
curve.push(p1);
|
||
|
||
return curve;
|
||
}
|
||
|
||
// Generate self-loop curve (parametric loop offset toward waypoint)
|
||
function generateSelfLoop(center, waypoint, resolution) {
|
||
const points = [];
|
||
const offset = [
|
||
waypoint[0] - center[0],
|
||
waypoint[1] - center[1],
|
||
waypoint[2] - center[2]
|
||
];
|
||
const offsetMag = Math.sqrt(offset[0]**2 + offset[1]**2 + offset[2]**2);
|
||
const norm = offsetMag > 0.01 ? offset.map(x => x / offsetMag) : [0, 1, 0];
|
||
|
||
// Create perpendicular vectors for the loop plane
|
||
let perp1, perp2;
|
||
if (Math.abs(norm[1]) < 0.9) {
|
||
perp1 = [norm[2], 0, -norm[0]];
|
||
} else {
|
||
perp1 = [0, norm[2], -norm[1]];
|
||
}
|
||
const mag1 = Math.sqrt(perp1[0]**2 + perp1[1]**2 + perp1[2]**2);
|
||
perp1 = perp1.map(x => x / mag1);
|
||
|
||
perp2 = [
|
||
norm[1]*perp1[2] - norm[2]*perp1[1],
|
||
norm[2]*perp1[0] - norm[0]*perp1[2],
|
||
norm[0]*perp1[1] - norm[1]*perp1[0]
|
||
];
|
||
|
||
const loopRadius = Math.min(offsetMag * 0.6, 0.8);
|
||
const loopCenter = [
|
||
center[0] + norm[0] * offsetMag * 0.4,
|
||
center[1] + norm[1] * offsetMag * 0.4,
|
||
center[2] + norm[2] * offsetMag * 0.4
|
||
];
|
||
|
||
for (let i = 0; i <= resolution; i++) {
|
||
const t = (i / resolution) * 2 * Math.PI;
|
||
const x = loopCenter[0] + loopRadius * (Math.cos(t) * perp1[0] + Math.sin(t) * perp2[0]);
|
||
const y = loopCenter[1] + loopRadius * (Math.cos(t) * perp1[1] + Math.sin(t) * perp2[1]);
|
||
const z = loopCenter[2] + loopRadius * (Math.cos(t) * perp1[2] + Math.sin(t) * perp2[2]);
|
||
points.push([x, y, z]);
|
||
}
|
||
|
||
return points;
|
||
}
|
||
|
||
function ZigzagRenderer() {
|
||
const containerRef = React.useRef(null);
|
||
const sceneRef = React.useRef(null);
|
||
const cameraRef = React.useRef(null);
|
||
const rendererRef = React.useRef(null);
|
||
const wireObjectsRef = React.useRef([]);
|
||
const waypointObjectsRef = React.useRef([]);
|
||
const surfaceObjectsRef = React.useRef([]);
|
||
const labelObjectsRef = React.useRef([]);
|
||
|
||
const [tension, setTension] = React.useState(1.0);
|
||
const [bending, setBending] = React.useState(0.5);
|
||
const [kappa, setKappa] = React.useState(2.0);
|
||
const [resolution, setResolution] = React.useState(20);
|
||
const [showWaypoints, setShowWaypoints] = React.useState(true);
|
||
const [showSurfaces, setShowSurfaces] = React.useState(true);
|
||
const [showLabels, setShowLabels] = React.useState(true);
|
||
|
||
// Orbit controls state
|
||
const orbitRef = React.useRef({
|
||
theta: Math.PI / 4,
|
||
phi: Math.PI / 4,
|
||
radius: 12,
|
||
target: [2, 2, 2],
|
||
isDragging: false,
|
||
lastX: 0,
|
||
lastY: 0
|
||
});
|
||
|
||
// Initialize Three.js scene
|
||
React.useEffect(() => {
|
||
if (!containerRef.current || !window.THREE) return;
|
||
|
||
const THREE = window.THREE;
|
||
const width = containerRef.current.clientWidth;
|
||
const height = containerRef.current.clientHeight;
|
||
|
||
// Scene
|
||
const scene = new THREE.Scene();
|
||
scene.background = new THREE.Color(0x0a0a0f);
|
||
sceneRef.current = scene;
|
||
|
||
// Camera
|
||
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100);
|
||
cameraRef.current = camera;
|
||
|
||
// Renderer
|
||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||
renderer.setSize(width, height);
|
||
renderer.setPixelRatio(window.devicePixelRatio);
|
||
containerRef.current.appendChild(renderer.domElement);
|
||
rendererRef.current = renderer;
|
||
|
||
// Lights
|
||
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
|
||
scene.add(ambient);
|
||
|
||
const directional = new THREE.DirectionalLight(0xffffff, 0.8);
|
||
directional.position.set(5, 10, 5);
|
||
scene.add(directional);
|
||
|
||
// Add vertices as glowing spheres
|
||
const vertexGeom = new THREE.SphereGeometry(0.15, 32, 32);
|
||
const vertexMat = new THREE.MeshStandardMaterial({
|
||
color: 0xf0c040,
|
||
emissive: 0xf0c040,
|
||
emissiveIntensity: 0.5
|
||
});
|
||
|
||
GEOMETRY_DATA.vertices.forEach(v => {
|
||
const pos = mapCoords(v.coords);
|
||
const mesh = new THREE.Mesh(vertexGeom, vertexMat);
|
||
mesh.position.set(pos[0], pos[1], pos[2]);
|
||
scene.add(mesh);
|
||
});
|
||
|
||
// Grid helper
|
||
const gridHelper = new THREE.GridHelper(10, 10, 0x333344, 0x222233);
|
||
gridHelper.position.y = -0.5;
|
||
scene.add(gridHelper);
|
||
|
||
// Axes helper
|
||
const axesHelper = new THREE.AxesHelper(3);
|
||
scene.add(axesHelper);
|
||
|
||
// Mouse controls
|
||
const canvas = renderer.domElement;
|
||
|
||
canvas.addEventListener('mousedown', (e) => {
|
||
orbitRef.current.isDragging = true;
|
||
orbitRef.current.lastX = e.clientX;
|
||
orbitRef.current.lastY = e.clientY;
|
||
});
|
||
|
||
canvas.addEventListener('mousemove', (e) => {
|
||
if (!orbitRef.current.isDragging) return;
|
||
const dx = e.clientX - orbitRef.current.lastX;
|
||
const dy = e.clientY - orbitRef.current.lastY;
|
||
orbitRef.current.theta -= dx * 0.01;
|
||
orbitRef.current.phi = Math.max(0.1, Math.min(Math.PI - 0.1, orbitRef.current.phi + dy * 0.01));
|
||
orbitRef.current.lastX = e.clientX;
|
||
orbitRef.current.lastY = e.clientY;
|
||
});
|
||
|
||
canvas.addEventListener('mouseup', () => {
|
||
orbitRef.current.isDragging = false;
|
||
});
|
||
|
||
canvas.addEventListener('mouseleave', () => {
|
||
orbitRef.current.isDragging = false;
|
||
});
|
||
|
||
canvas.addEventListener('wheel', (e) => {
|
||
e.preventDefault();
|
||
orbitRef.current.radius = Math.max(3, Math.min(30, orbitRef.current.radius + e.deltaY * 0.01));
|
||
});
|
||
|
||
// Animation loop
|
||
function animate() {
|
||
requestAnimationFrame(animate);
|
||
|
||
const orbit = orbitRef.current;
|
||
const x = orbit.target[0] + orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta);
|
||
const y = orbit.target[1] + orbit.radius * Math.cos(orbit.phi);
|
||
const z = orbit.target[2] + orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta);
|
||
|
||
camera.position.set(x, y, z);
|
||
camera.lookAt(orbit.target[0], orbit.target[1], orbit.target[2]);
|
||
|
||
renderer.render(scene, camera);
|
||
}
|
||
animate();
|
||
|
||
// Resize handler
|
||
const handleResize = () => {
|
||
const w = containerRef.current.clientWidth;
|
||
const h = containerRef.current.clientHeight;
|
||
camera.aspect = w / h;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(w, h);
|
||
};
|
||
window.addEventListener('resize', handleResize);
|
||
|
||
return () => {
|
||
window.removeEventListener('resize', handleResize);
|
||
renderer.dispose();
|
||
if (containerRef.current && renderer.domElement) {
|
||
containerRef.current.removeChild(renderer.domElement);
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// Update wires when parameters change
|
||
React.useEffect(() => {
|
||
if (!sceneRef.current || !window.THREE) return;
|
||
|
||
const THREE = window.THREE;
|
||
const scene = sceneRef.current;
|
||
|
||
// Remove old wire objects
|
||
wireObjectsRef.current.forEach(obj => scene.remove(obj));
|
||
wireObjectsRef.current = [];
|
||
|
||
// Create wire lookup for surfaces
|
||
const wireById = {};
|
||
|
||
// Add wires
|
||
GEOMETRY_DATA.wires.forEach(wire => {
|
||
const p0 = mapCoords(wire.endpoint_coords[0]);
|
||
const p1 = mapCoords(wire.endpoint_coords[1]);
|
||
const waypoint = mapCoords(wire.coords);
|
||
|
||
const group = getWireGroup(wire);
|
||
const color = WIRE_COLORS[group];
|
||
|
||
let curvePoints;
|
||
if (wire.endpoints[0] === wire.endpoints[1]) {
|
||
// Self-loop
|
||
curvePoints = generateSelfLoop(p0, waypoint, resolution);
|
||
} else {
|
||
// Elastic curve
|
||
curvePoints = solveElasticCurve(p0, p1, waypoint, tension, bending, kappa, resolution);
|
||
}
|
||
|
||
wireById[wire.id] = curvePoints;
|
||
|
||
// Create tube geometry
|
||
const points = curvePoints.map(p => new THREE.Vector3(p[0], p[1], p[2]));
|
||
const curve = new THREE.CatmullRomCurve3(points);
|
||
const tubeGeom = new THREE.TubeGeometry(curve, resolution * 2, 0.04, 8, false);
|
||
const tubeMat = new THREE.MeshStandardMaterial({
|
||
color: color,
|
||
emissive: color,
|
||
emissiveIntensity: 0.3
|
||
});
|
||
const tube = new THREE.Mesh(tubeGeom, tubeMat);
|
||
scene.add(tube);
|
||
wireObjectsRef.current.push(tube);
|
||
});
|
||
|
||
// Store wire paths for surface rendering
|
||
sceneRef.current.userData.wireById = wireById;
|
||
|
||
}, [tension, bending, kappa, resolution]);
|
||
|
||
// Update waypoint visibility
|
||
React.useEffect(() => {
|
||
if (!sceneRef.current || !window.THREE) return;
|
||
|
||
const THREE = window.THREE;
|
||
const scene = sceneRef.current;
|
||
|
||
// Remove old waypoint objects
|
||
waypointObjectsRef.current.forEach(obj => scene.remove(obj));
|
||
waypointObjectsRef.current = [];
|
||
|
||
if (showWaypoints) {
|
||
const waypointGeom = new THREE.SphereGeometry(0.08, 16, 16);
|
||
const waypointMat = new THREE.MeshStandardMaterial({
|
||
color: 0xf0c040,
|
||
emissive: 0xf0c040,
|
||
emissiveIntensity: 0.6,
|
||
transparent: true,
|
||
opacity: 0.8
|
||
});
|
||
|
||
GEOMETRY_DATA.wires.forEach(wire => {
|
||
const pos = mapCoords(wire.coords);
|
||
const mesh = new THREE.Mesh(waypointGeom, waypointMat);
|
||
mesh.position.set(pos[0], pos[1], pos[2]);
|
||
scene.add(mesh);
|
||
waypointObjectsRef.current.push(mesh);
|
||
});
|
||
}
|
||
}, [showWaypoints]);
|
||
|
||
// Update surface visibility
|
||
React.useEffect(() => {
|
||
if (!sceneRef.current || !window.THREE) return;
|
||
|
||
const THREE = window.THREE;
|
||
const scene = sceneRef.current;
|
||
|
||
// Remove old surface objects
|
||
surfaceObjectsRef.current.forEach(obj => scene.remove(obj));
|
||
surfaceObjectsRef.current = [];
|
||
|
||
if (showSurfaces) {
|
||
const surfaceMat = new THREE.MeshStandardMaterial({
|
||
color: 0xff6b6b,
|
||
emissive: 0xff6b6b,
|
||
emissiveIntensity: 0.4,
|
||
transparent: true,
|
||
opacity: 0.6
|
||
});
|
||
|
||
const lineMat = new THREE.LineBasicMaterial({
|
||
color: 0xff6b6b,
|
||
transparent: true,
|
||
opacity: 0.4
|
||
});
|
||
|
||
GEOMETRY_DATA.surfaces.forEach(surface => {
|
||
const pos = mapCoords(surface.coords);
|
||
|
||
// Surface center point
|
||
const sphereGeom = new THREE.SphereGeometry(0.06, 12, 12);
|
||
const sphere = new THREE.Mesh(sphereGeom, surfaceMat);
|
||
sphere.position.set(pos[0], pos[1], pos[2]);
|
||
scene.add(sphere);
|
||
surfaceObjectsRef.current.push(sphere);
|
||
|
||
// Lines to boundary wire midpoints
|
||
const wireById = scene.userData.wireById || {};
|
||
surface.boundary_wires.forEach(wireId => {
|
||
const wire = GEOMETRY_DATA.wires.find(w => w.id === wireId);
|
||
if (wire) {
|
||
const wirePos = mapCoords(wire.coords);
|
||
const geometry = new THREE.BufferGeometry().setFromPoints([
|
||
new THREE.Vector3(pos[0], pos[1], pos[2]),
|
||
new THREE.Vector3(wirePos[0], wirePos[1], wirePos[2])
|
||
]);
|
||
const line = new THREE.Line(geometry, lineMat);
|
||
scene.add(line);
|
||
surfaceObjectsRef.current.push(line);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}, [showSurfaces, tension, bending, kappa, resolution]);
|
||
|
||
// Update label visibility
|
||
React.useEffect(() => {
|
||
if (!sceneRef.current || !window.THREE) return;
|
||
|
||
const THREE = window.THREE;
|
||
const scene = sceneRef.current;
|
||
|
||
// Remove old label objects
|
||
labelObjectsRef.current.forEach(obj => scene.remove(obj));
|
||
labelObjectsRef.current = [];
|
||
|
||
if (showLabels) {
|
||
// Create canvas-based sprite labels
|
||
GEOMETRY_DATA.vertices.forEach(v => {
|
||
const canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
canvas.width = 128;
|
||
canvas.height = 64;
|
||
|
||
ctx.fillStyle = 'rgba(10, 10, 15, 0.8)';
|
||
ctx.fillRect(0, 0, 128, 64);
|
||
ctx.strokeStyle = '#f0c040';
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(2, 2, 124, 60);
|
||
|
||
ctx.font = 'bold 20px monospace';
|
||
ctx.fillStyle = '#f0c040';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(v.label, 64, 32);
|
||
|
||
const texture = new THREE.CanvasTexture(canvas);
|
||
const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
||
const sprite = new THREE.Sprite(spriteMat);
|
||
|
||
const pos = mapCoords(v.coords);
|
||
sprite.position.set(pos[0], pos[1] + 0.4, pos[2]);
|
||
sprite.scale.set(1, 0.5, 1);
|
||
|
||
scene.add(sprite);
|
||
labelObjectsRef.current.push(sprite);
|
||
});
|
||
}
|
||
}, [showLabels]);
|
||
|
||
const panelStyle = {
|
||
position: 'absolute',
|
||
top: '20px',
|
||
left: '20px',
|
||
background: 'rgba(10, 10, 20, 0.85)',
|
||
backdropFilter: 'blur(10px)',
|
||
padding: '20px',
|
||
borderRadius: '12px',
|
||
color: '#e0e0e0',
|
||
fontFamily: 'monospace',
|
||
fontSize: '14px',
|
||
border: '1px solid rgba(240, 192, 64, 0.3)',
|
||
minWidth: '220px'
|
||
};
|
||
|
||
const sliderStyle = {
|
||
width: '100%',
|
||
marginTop: '4px',
|
||
accentColor: '#f0c040'
|
||
};
|
||
|
||
const checkboxStyle = {
|
||
accentColor: '#f0c040',
|
||
marginRight: '8px'
|
||
};
|
||
|
||
return React.createElement('div', { style: { width: '100vw', height: '100vh', position: 'relative' } },
|
||
React.createElement('div', { ref: containerRef, style: { width: '100%', height: '100%' } }),
|
||
React.createElement('div', { style: panelStyle },
|
||
React.createElement('h3', { style: { margin: '0 0 15px 0', color: '#f0c040' } }, 'Zigzag Renderer'),
|
||
|
||
React.createElement('div', { style: { marginBottom: '12px' } },
|
||
React.createElement('label', null, `Tension τ: ${tension.toFixed(2)}`),
|
||
React.createElement('input', {
|
||
type: 'range', min: '0.1', max: '5', step: '0.1', value: tension,
|
||
onChange: (e) => setTension(parseFloat(e.target.value)),
|
||
style: sliderStyle
|
||
})
|
||
),
|
||
|
||
React.createElement('div', { style: { marginBottom: '12px' } },
|
||
React.createElement('label', null, `Bending β: ${bending.toFixed(2)}`),
|
||
React.createElement('input', {
|
||
type: 'range', min: '0', max: '2', step: '0.05', value: bending,
|
||
onChange: (e) => setBending(parseFloat(e.target.value)),
|
||
style: sliderStyle
|
||
})
|
||
),
|
||
|
||
React.createElement('div', { style: { marginBottom: '12px' } },
|
||
React.createElement('label', null, `Waypoint κ: ${kappa.toFixed(2)}`),
|
||
React.createElement('input', {
|
||
type: 'range', min: '0', max: '10', step: '0.1', value: kappa,
|
||
onChange: (e) => setKappa(parseFloat(e.target.value)),
|
||
style: sliderStyle
|
||
})
|
||
),
|
||
|
||
React.createElement('div', { style: { marginBottom: '12px' } },
|
||
React.createElement('label', null, `Resolution: ${resolution}`),
|
||
React.createElement('input', {
|
||
type: 'range', min: '5', max: '50', step: '1', value: resolution,
|
||
onChange: (e) => setResolution(parseInt(e.target.value)),
|
||
style: sliderStyle
|
||
})
|
||
),
|
||
|
||
React.createElement('hr', { style: { border: 'none', borderTop: '1px solid rgba(240, 192, 64, 0.3)', margin: '15px 0' } }),
|
||
|
||
React.createElement('div', { style: { marginBottom: '8px' } },
|
||
React.createElement('label', null,
|
||
React.createElement('input', {
|
||
type: 'checkbox', checked: showWaypoints,
|
||
onChange: (e) => setShowWaypoints(e.target.checked),
|
||
style: checkboxStyle
|
||
}),
|
||
'Waypoints'
|
||
)
|
||
),
|
||
|
||
React.createElement('div', { style: { marginBottom: '8px' } },
|
||
React.createElement('label', null,
|
||
React.createElement('input', {
|
||
type: 'checkbox', checked: showSurfaces,
|
||
onChange: (e) => setShowSurfaces(e.target.checked),
|
||
style: checkboxStyle
|
||
}),
|
||
'Surfaces'
|
||
)
|
||
),
|
||
|
||
React.createElement('div', { style: { marginBottom: '8px' } },
|
||
React.createElement('label', null,
|
||
React.createElement('input', {
|
||
type: 'checkbox', checked: showLabels,
|
||
onChange: (e) => setShowLabels(e.target.checked),
|
||
style: checkboxStyle
|
||
}),
|
||
'Labels'
|
||
)
|
||
),
|
||
|
||
React.createElement('hr', { style: { border: 'none', borderTop: '1px solid rgba(240, 192, 64, 0.3)', margin: '15px 0' } }),
|
||
|
||
React.createElement('div', { style: { fontSize: '11px', color: '#888' } },
|
||
React.createElement('div', null, '🔵 Input (r0)'),
|
||
React.createElement('div', null, '🟣 Crossing (s0)'),
|
||
React.createElement('div', null, '🟢 Output (r1)'),
|
||
React.createElement('div', null, '🟠 Self-loop')
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
// Export for use
|
||
window.ZigzagRenderer = ZigzagRenderer;
|