diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..4dbf433
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+ Zigzag Renderer - Half Braid Geometry
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/zigzag-renderer.jsx b/web/zigzag-renderer.jsx
new file mode 100644
index 0000000..5a24b67
--- /dev/null
+++ b/web/zigzag-renderer.jsx
@@ -0,0 +1,708 @@
+// Three.js renderer for zigzag-engine half_braid geometry
+// Renders vertices, wires (elastic curves), and surfaces from explosion poset
+
+const GEOMETRY_DATA = {
+ "metadata": {
+ "source": "half_braid.json",
+ "dimension": 3,
+ "total_points": 23,
+ "total_covers": 35
+ },
+ "vertices": [
+ { "id": 21, "label": "vertex_0", "point": "s0,s0,s0", "coords": [1.0, 1.0, 1.0] },
+ { "id": 22, "label": "vertex_1", "point": "s1,s0,s0", "coords": [3.0, 1.0, 1.0] }
+ ],
+ "wires": [
+ { "id": 5, "label": "wire_0", "point": "s0,s0,r0", "coords": [1.0, 1.0, 0.0],
+ "endpoints": [21, 22], "endpoint_coords": [[1.0, 1.0, 1.0], [3.0, 1.0, 1.0]] },
+ { "id": 8, "label": "wire_1", "point": "s0,s1,r0", "coords": [1.0, 3.0, 0.0],
+ "endpoints": [21, 22], "endpoint_coords": [[1.0, 1.0, 1.0], [3.0, 1.0, 1.0]] },
+ { "id": 14, "label": "wire_2", "point": "s0,s0,r1", "coords": [1.0, 1.0, 2.0],
+ "endpoints": [21, 21], "endpoint_coords": [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]] },
+ { "id": 15, "label": "wire_3", "point": "s1,s0,r1", "coords": [3.0, 1.0, 2.0],
+ "endpoints": [21, 22], "endpoint_coords": [[1.0, 1.0, 1.0], [3.0, 1.0, 1.0]] },
+ { "id": 18, "label": "wire_4", "point": "r0,s0,s0", "coords": [0.0, 1.0, 1.0],
+ "endpoints": [21, 22], "endpoint_coords": [[1.0, 1.0, 1.0], [3.0, 1.0, 1.0]] },
+ { "id": 19, "label": "wire_5", "point": "r1,s0,s0", "coords": [2.0, 1.0, 1.0],
+ "endpoints": [21, 22], "endpoint_coords": [[1.0, 1.0, 1.0], [3.0, 1.0, 1.0]] },
+ { "id": 20, "label": "wire_6", "point": "r2,s0,s0", "coords": [4.0, 1.0, 1.0],
+ "endpoints": [21, 22], "endpoint_coords": [[1.0, 1.0, 1.0], [3.0, 1.0, 1.0]] }
+ ],
+ "surfaces": [
+ { "id": 3, "label": "surface_0", "point": "r0,s0,r0", "coords": [0.0, 1.0, 0.0], "boundary_wires": [5, 8, 14, 15, 18, 19, 20] },
+ { "id": 4, "label": "surface_1", "point": "r1,s0,r0", "coords": [2.0, 1.0, 0.0], "boundary_wires": [5, 15, 19, 20] },
+ { "id": 6, "label": "surface_2", "point": "r0,s1,r0", "coords": [0.0, 3.0, 0.0], "boundary_wires": [8, 14, 15, 18, 19, 20] },
+ { "id": 7, "label": "surface_3", "point": "r1,s1,r0", "coords": [2.0, 3.0, 0.0], "boundary_wires": [8, 15, 19, 20] },
+ { "id": 11, "label": "surface_4", "point": "r0,s0,r1", "coords": [0.0, 1.0, 2.0], "boundary_wires": [14, 15, 18] },
+ { "id": 12, "label": "surface_5", "point": "r1,s0,r1", "coords": [2.0, 1.0, 2.0], "boundary_wires": [5, 8, 14, 15, 18, 19] },
+ { "id": 13, "label": "surface_6", "point": "r2,s0,r1", "coords": [4.0, 1.0, 2.0], "boundary_wires": [5, 8, 14, 15, 18, 19, 20] },
+ { "id": 16, "label": "surface_7", "point": "r0,r0,s0", "coords": [0.0, 0.0, 1.0], "boundary_wires": [14, 15, 18, 19, 20] },
+ { "id": 17, "label": "surface_8", "point": "r0,r1,s0", "coords": [0.0, 2.0, 1.0], "boundary_wires": [18] }
+ ],
+ "volumes": [
+ { "id": 0, "label": "volume_0", "point": "r0,r0,r0", "coords": [0.0, 0.0, 0.0] },
+ { "id": 1, "label": "volume_1", "point": "r0,r1,r0", "coords": [0.0, 2.0, 0.0] },
+ { "id": 2, "label": "volume_2", "point": "r0,r2,r0", "coords": [0.0, 4.0, 0.0] },
+ { "id": 9, "label": "volume_3", "point": "r0,r0,r1", "coords": [0.0, 0.0, 2.0] },
+ { "id": 10, "label": "volume_4", "point": "r0,r1,r1", "coords": [0.0, 2.0, 2.0] }
+ ]
+};
+
+// Coordinate mapping: coord[0]→z, coord[1]→y, coord[2]→x, scale 1.2
+const SCALE = 1.2;
+function mapCoords(coords) {
+ return [coords[2] * SCALE, coords[1] * SCALE, coords[0] * 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;