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 + + + +
+
Loading Three.js...
+
+ + + + + + + + + + + + + + + + + + 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;