diff --git a/examples/render_braiding.rs b/examples/render_braiding.rs index ea1c7ed..a63090c 100644 --- a/examples/render_braiding.rs +++ b/examples/render_braiding.rs @@ -2,13 +2,15 @@ //! //! Run with: cargo run --example render_braiding //! -//! Outputs fixtures/half_braid_geometry.json with vertices, wires, surfaces, volumes. +//! Outputs fixtures/half_braid_geometry.json with VISIBLE elements only. +//! Visibility follows homotopy.io's rule: a point at geom_dim d is visible +//! iff coords[d..] are all singular. use std::collections::{HashMap, HashSet, VecDeque}; use std::fs; -use zigzag_engine::diagram::{Diagram, DiagramN}; -use zigzag_engine::explosion::{HeightLabel, Point, Poset}; +use zigzag_engine::diagram::Diagram; +use zigzag_engine::explosion::{HeightLabel, Point}; use zigzag_engine::import::load_homotopy_diagram_n; use serde::Serialize; @@ -19,7 +21,6 @@ struct Geometry { vertices: Vec, wires: Vec, surfaces: Vec, - volumes: Vec, } #[derive(Serialize)] @@ -27,6 +28,7 @@ struct Metadata { source: String, dimension: usize, total_points: usize, + visible_points: usize, total_covers: usize, } @@ -43,9 +45,9 @@ struct Wire { id: usize, label: String, point: String, - coords: [f64; 3], // The wire's own coordinate (midpoint/waypoint) - endpoints: [usize; 2], // Vertex IDs - endpoint_coords: [[f64; 3]; 2], // For convenience in renderer + coords: [f64; 3], + endpoints: [usize; 2], + endpoint_coords: [[f64; 3]; 2], } #[derive(Serialize)] @@ -54,15 +56,7 @@ struct Surface { label: String, point: String, coords: [f64; 3], - boundary_wires: Vec, // Wire IDs on boundary -} - -#[derive(Serialize)] -struct Volume { - id: usize, - label: String, - point: String, - coords: [f64; 3], + boundary_wires: Vec, } /// Format a point as a string like "s0,s1,r0" @@ -76,15 +70,37 @@ fn format_point(p: &Point) -> String { .join(",") } -/// Compute linear coordinates from height labels -fn linear_coords(p: &Point) -> [f64; 3] { - let coords: Vec = p.0.iter().map(|h| h.to_linear_index() as f64).collect(); - // Pad to 3D if needed - [ - coords.get(0).copied().unwrap_or(0.0), - coords.get(1).copied().unwrap_or(0.0), - coords.get(2).copied().unwrap_or(0.0), - ] +/// Compute layout coordinates for rendering. +/// +/// For half_braid visible elements: +/// - coord[0] (depth/strand): r0→-1, r1→0, r2→1 for wires; s0→-0.5, s1→0.5 for vertices +/// - coord[1] (height): r0→-1, s0→0, r1→1 for layout y +/// - coord[2] (time): s0→0 for visible elements (all at crossing time) +/// +/// Output: [x, y, z] where: +/// - x = time (all 0 for visible crossing slice) +/// - y = height +/// - z = depth (spread wires/vertices along this axis) +fn layout_coords(p: &Point) -> [f64; 3] { + // For the visible crossing slice, all elements have coord[2] = s0 (time = singular) + // So x (time) = 0 for all visible elements + + // z = depth axis (coord[0]) + let z = match &p.0[0] { + HeightLabel::Regular(j) => (*j as f64) - 1.0, // r0→-1, r1→0, r2→1 + HeightLabel::Singular(j) => (*j as f64) - 0.5, // s0→-0.5, s1→0.5 + }; + + // y = height axis (coord[1]) + let y = match &p.0[1] { + HeightLabel::Regular(j) => (*j as f64) - 1.0, // r0→-1, r1→1 + HeightLabel::Singular(j) => *j as f64, // s0→0 + }; + + // x = time axis (coord[2]) - all visible elements are at s0 + let x = 0.0; + + [x, y, z] } /// Count singular labels in a point @@ -111,14 +127,25 @@ fn main() { eprintln!("Loaded half_braid.json: dim={}, {} points, {} covers", n, pts.len(), pts.covers().len()); - // Group points by geometric dimension + // Filter to VISIBLE points only + let visible_indices: Vec = pts.elements() + .iter() + .enumerate() + .filter(|(_, point)| point.is_visible(n)) + .map(|(idx, _)| idx) + .collect(); + + eprintln!("Visible points: {}", visible_indices.len()); + + // Group visible points by geometric dimension let mut by_geom_dim: HashMap> = HashMap::new(); - for (idx, point) in pts.elements().iter().enumerate() { + for &idx in &visible_indices { + let point = &pts.elements()[idx]; let gd = geom_dim(point, n); by_geom_dim.entry(gd).or_default().push(idx); } - // Build adjacency for reachability + // Build adjacency for reachability (on full poset) let mut successors: Vec> = vec![vec![]; pts.len()]; let mut predecessors: Vec> = vec![vec![]; pts.len()]; for &(lower, upper) in pts.covers() { @@ -126,7 +153,7 @@ fn main() { predecessors[upper].push(lower); } - // Helper: find all transitively reachable points (for wire→vertex connections) + // Helper: find all transitively reachable points let reachable_from = |start: usize, adj: &[Vec]| -> HashSet { let mut visited = HashSet::new(); let mut queue = VecDeque::new(); @@ -142,12 +169,13 @@ fn main() { visited }; - // Get vertex indices + // Get visible vertex indices let vertex_indices = by_geom_dim.get(&0).map(|v| v.as_slice()).unwrap_or(&[]); let vertex_set: HashSet = vertex_indices.iter().copied().collect(); - // Get wire indices + // Get visible wire indices let wire_indices = by_geom_dim.get(&1).map(|v| v.as_slice()).unwrap_or(&[]); + let wire_set: HashSet = wire_indices.iter().copied().collect(); // Build vertices let mut vertices: Vec = Vec::new(); @@ -157,7 +185,7 @@ fn main() { id: idx, label: format!("vertex_{}", i), point: format_point(point), - coords: linear_coords(point), + coords: layout_coords(point), }); } @@ -166,8 +194,7 @@ fn main() { for (i, &idx) in wire_indices.iter().enumerate() { let point = &pts.elements()[idx]; - // Find connected vertices via TRANSITIVE reachability - // A wire (strand) spans between vertices even if the poset path has intermediate points + // Find connected VISIBLE vertices via transitive reachability let reachable_up = reachable_from(idx, &successors); let reachable_down = reachable_from(idx, &predecessors); @@ -179,43 +206,38 @@ fn main() { connected.sort(); connected.dedup(); - // Default to first two vertices if we don't have exactly 2 let endpoints = if connected.len() >= 2 { [connected[0], connected[1]] } else if connected.len() == 1 { - // Wire connects to only one vertex - use same vertex twice - // (This represents a loop or boundary wire) [connected[0], connected[0]] } else { - // No connected vertices found - use first two vertices as fallback [vertex_indices[0], vertex_indices.get(1).copied().unwrap_or(vertex_indices[0])] }; let endpoint_coords = [ - linear_coords(&pts.elements()[endpoints[0]]), - linear_coords(&pts.elements()[endpoints[1]]), + layout_coords(&pts.elements()[endpoints[0]]), + layout_coords(&pts.elements()[endpoints[1]]), ]; wires.push(Wire { id: idx, label: format!("wire_{}", i), point: format_point(point), - coords: linear_coords(point), + coords: layout_coords(point), endpoints, endpoint_coords, }); } - // Build surfaces (geom_dim=2) + // Build visible surfaces (geom_dim=2) let surface_indices = by_geom_dim.get(&2).map(|v| v.as_slice()).unwrap_or(&[]); - let wire_set: HashSet = wire_indices.iter().copied().collect(); let mut surfaces: Vec = Vec::new(); for (i, &idx) in surface_indices.iter().enumerate() { let point = &pts.elements()[idx]; - // Find boundary wires via DIRECT covering relations only - // Surface (geom_dim=2) connects to wires (geom_dim=1) via single covers + // Find boundary wires via DIRECT covering relations + // Filter to only VISIBLE wires let mut boundary_wires: Vec = successors[idx] .iter() .chain(predecessors[idx].iter()) @@ -223,42 +245,29 @@ fn main() { .copied() .collect(); boundary_wires.sort(); + boundary_wires.dedup(); surfaces.push(Surface { id: idx, label: format!("surface_{}", i), point: format_point(point), - coords: linear_coords(point), + coords: layout_coords(point), boundary_wires, }); } - // Build volumes (geom_dim=3) - let volume_indices = by_geom_dim.get(&3).map(|v| v.as_slice()).unwrap_or(&[]); - - let mut volumes: Vec = Vec::new(); - for (i, &idx) in volume_indices.iter().enumerate() { - let point = &pts.elements()[idx]; - volumes.push(Volume { - id: idx, - label: format!("volume_{}", i), - point: format_point(point), - coords: linear_coords(point), - }); - } - - // Build output + // Build output (no volumes - they're not rendered) let geometry = Geometry { metadata: Metadata { source: "half_braid.json".to_string(), dimension: n, total_points: pts.len(), + visible_points: visible_indices.len(), total_covers: pts.covers().len(), }, vertices, wires, surfaces, - volumes, }; // Output JSON @@ -268,11 +277,10 @@ fn main() { fs::write("fixtures/half_braid_geometry.json", &json_output) .expect("Failed to write fixtures/half_braid_geometry.json"); - eprintln!("\nWrote fixtures/half_braid_geometry.json"); + eprintln!("\nWrote fixtures/half_braid_geometry.json (VISIBLE ONLY)"); eprintln!(" {} vertices", geometry.vertices.len()); eprintln!(" {} wires", geometry.wires.len()); eprintln!(" {} surfaces", geometry.surfaces.len()); - eprintln!(" {} volumes", geometry.volumes.len()); // Also print to stdout for piping println!("{}", json_output); diff --git a/fixtures/half_braid_geometry.json b/fixtures/half_braid_geometry.json index ef0b440..23be593 100644 --- a/fixtures/half_braid_geometry.json +++ b/fixtures/half_braid_geometry.json @@ -3,6 +3,7 @@ "source": "half_braid.json", "dimension": 3, "total_points": 23, + "visible_points": 12, "total_covers": 35 }, "vertices": [ @@ -11,9 +12,9 @@ "label": "vertex_0", "point": "s0,s0,s0", "coords": [ - 1.0, - 1.0, - 1.0 + 0.0, + 0.0, + -0.5 ] }, { @@ -21,125 +22,21 @@ "label": "vertex_1", "point": "s1,s0,s0", "coords": [ - 3.0, - 1.0, - 1.0 + 0.0, + 0.0, + 0.5 ] } ], "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", + "label": "wire_0", "point": "r0,s0,s0", "coords": [ 0.0, - 1.0, - 1.0 + 0.0, + -1.0 ], "endpoints": [ 21, @@ -147,25 +44,25 @@ ], "endpoint_coords": [ [ - 1.0, - 1.0, - 1.0 + 0.0, + 0.0, + -0.5 ], [ - 3.0, - 1.0, - 1.0 + 0.0, + 0.0, + 0.5 ] ] }, { "id": 19, - "label": "wire_5", + "label": "wire_1", "point": "r1,s0,s0", "coords": [ - 2.0, - 1.0, - 1.0 + 0.0, + 0.0, + 0.0 ], "endpoints": [ 21, @@ -173,24 +70,24 @@ ], "endpoint_coords": [ [ - 1.0, - 1.0, - 1.0 + 0.0, + 0.0, + -0.5 ], [ - 3.0, - 1.0, - 1.0 + 0.0, + 0.0, + 0.5 ] ] }, { "id": 20, - "label": "wire_6", + "label": "wire_2", "point": "r2,s0,s0", "coords": [ - 4.0, - 1.0, + 0.0, + 0.0, 1.0 ], "endpoints": [ @@ -199,126 +96,27 @@ ], "endpoint_coords": [ [ - 1.0, - 1.0, - 1.0 + 0.0, + 0.0, + -0.5 ], [ - 3.0, - 1.0, - 1.0 + 0.0, + 0.0, + 0.5 ] ] } ], "surfaces": [ - { - "id": 3, - "label": "surface_0", - "point": "r0,s0,r0", - "coords": [ - 0.0, - 1.0, - 0.0 - ], - "boundary_wires": [ - 5, - 18 - ] - }, - { - "id": 4, - "label": "surface_1", - "point": "r1,s0,r0", - "coords": [ - 2.0, - 1.0, - 0.0 - ], - "boundary_wires": [ - 5, - 19 - ] - }, - { - "id": 6, - "label": "surface_2", - "point": "r0,s1,r0", - "coords": [ - 0.0, - 3.0, - 0.0 - ], - "boundary_wires": [ - 8, - 18 - ] - }, - { - "id": 7, - "label": "surface_3", - "point": "r1,s1,r0", - "coords": [ - 2.0, - 3.0, - 0.0 - ], - "boundary_wires": [ - 8, - 19 - ] - }, - { - "id": 11, - "label": "surface_4", - "point": "r0,s0,r1", - "coords": [ - 0.0, - 1.0, - 2.0 - ], - "boundary_wires": [ - 14, - 18 - ] - }, - { - "id": 12, - "label": "surface_5", - "point": "r1,s0,r1", - "coords": [ - 2.0, - 1.0, - 2.0 - ], - "boundary_wires": [ - 14, - 15, - 19 - ] - }, - { - "id": 13, - "label": "surface_6", - "point": "r2,s0,r1", - "coords": [ - 4.0, - 1.0, - 2.0 - ], - "boundary_wires": [ - 15, - 20 - ] - }, { "id": 16, - "label": "surface_7", + "label": "surface_0", "point": "r0,r0,s0", "coords": [ 0.0, - 0.0, - 1.0 + -1.0, + -1.0 ], "boundary_wires": [ 18 @@ -326,68 +124,16 @@ }, { "id": 17, - "label": "surface_8", + "label": "surface_1", "point": "r0,r1,s0", "coords": [ 0.0, - 2.0, - 1.0 + 0.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 - ] - } ] } \ No newline at end of file diff --git a/src/explosion.rs b/src/explosion.rs index 3c15abc..063210c 100644 --- a/src/explosion.rs +++ b/src/explosion.rs @@ -120,6 +120,19 @@ impl Point { labels.push(label); Self(labels) } + + /// Check if this point is visible for rendering. + /// + /// A point at geometric dimension d is visible iff coords[d..] are all singular. + /// This matches homotopy.io's visibility filter (mesh.rs:111-115). + /// + /// Geometric dimension = total_dim - singular_count, where singular_count + /// is the number of singular labels in the point. + pub fn is_visible(&self, total_dim: usize) -> bool { + let singular_count = self.0.iter().filter(|h| h.is_singular()).count(); + let geom_dim = total_dim - singular_count; + self.0.iter().skip(geom_dim).all(|h| h.is_singular()) + } } /// A poset: a set with a partial order. diff --git a/web/zigzag-renderer.jsx b/web/zigzag-renderer.jsx index c7fb2ae..6b9bf74 100644 --- a/web/zigzag-renderer.jsx +++ b/web/zigzag-renderer.jsx @@ -1,57 +1,38 @@ // Three.js renderer for zigzag-engine half_braid geometry -// Renders vertices, wires (elastic curves), and surfaces from explosion poset +// 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": [1.0, 1.0, 1.0] }, - { "id": 22, "label": "vertex_1", "point": "s1,s0,s0", "coords": [3.0, 1.0, 1.0] } + { "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": 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]] } + { "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": 3, "label": "surface_0", "point": "r0,s0,r0", "coords": [0.0, 1.0, 0.0], "boundary_wires": [5, 18] }, - { "id": 4, "label": "surface_1", "point": "r1,s0,r0", "coords": [2.0, 1.0, 0.0], "boundary_wires": [5, 19] }, - { "id": 6, "label": "surface_2", "point": "r0,s1,r0", "coords": [0.0, 3.0, 0.0], "boundary_wires": [8, 18] }, - { "id": 7, "label": "surface_3", "point": "r1,s1,r0", "coords": [2.0, 3.0, 0.0], "boundary_wires": [8, 19] }, - { "id": 11, "label": "surface_4", "point": "r0,s0,r1", "coords": [0.0, 1.0, 2.0], "boundary_wires": [14, 18] }, - { "id": 12, "label": "surface_5", "point": "r1,s0,r1", "coords": [2.0, 1.0, 2.0], "boundary_wires": [14, 15, 19] }, - { "id": 13, "label": "surface_6", "point": "r2,s0,r1", "coords": [4.0, 1.0, 2.0], "boundary_wires": [15, 20] }, - { "id": 16, "label": "surface_7", "point": "r0,r0,s0", "coords": [0.0, 0.0, 1.0], "boundary_wires": [18] }, - { "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] } + { "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: coord[0]→z, coord[1]→y, coord[2]→x, scale 1.2 -const SCALE = 1.2; +// Coordinate mapping: coords are already [x, y, z] layout coordinates +// Scale for better visualization +const SCALE = 2.0; function mapCoords(coords) { - return [coords[2] * SCALE, coords[1] * SCALE, coords[0] * SCALE]; + return [coords[0] * SCALE, coords[1] * SCALE, coords[2] * SCALE]; } // Wire colors by group