//! Generate geometry JSON from explosion for Three.js rendering. //! //! Run with: cargo run --example render_braiding //! //! 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; use zigzag_engine::explosion::{HeightLabel, Point}; use zigzag_engine::import::load_homotopy_diagram_n; use serde::Serialize; #[derive(Serialize)] struct Geometry { metadata: Metadata, vertices: Vec, wires: Vec, surfaces: Vec, } #[derive(Serialize)] struct Metadata { source: String, dimension: usize, total_points: usize, visible_points: usize, total_covers: usize, } #[derive(Serialize)] struct Vertex { id: usize, label: String, point: String, coords: [f64; 3], } #[derive(Serialize)] struct Wire { id: usize, label: String, point: String, coords: [f64; 3], endpoints: [usize; 2], endpoint_coords: [[f64; 3]; 2], } #[derive(Serialize)] struct Surface { id: usize, label: String, point: String, coords: [f64; 3], boundary_wires: Vec, } /// Format a point as a string like "s0,s1,r0" fn format_point(p: &Point) -> String { p.0.iter() .map(|h| match h { HeightLabel::Regular(j) => format!("r{}", j), HeightLabel::Singular(j) => format!("s{}", j), }) .collect::>() .join(",") } /// 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 fn singular_count(p: &Point) -> usize { p.0.iter().filter(|h| h.is_singular()).count() } /// Compute geometric dimension from singular count fn geom_dim(p: &Point, n: usize) -> usize { n - singular_count(p) } fn main() { // Load diagram let json = fs::read_to_string("fixtures/half_braid.json") .expect("Failed to read fixtures/half_braid.json"); let diagram_n = load_homotopy_diagram_n(&json) .expect("Failed to parse half_braid.json"); let diagram = Diagram::DiagramN(diagram_n); let n = diagram.dimension(); let pts = diagram.full_points(); eprintln!("Loaded half_braid.json: dim={}, {} points, {} covers", n, pts.len(), pts.covers().len()); // 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 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 (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() { successors[lower].push(upper); predecessors[upper].push(lower); } // Helper: find all transitively reachable points let reachable_from = |start: usize, adj: &[Vec]| -> HashSet { let mut visited = HashSet::new(); let mut queue = VecDeque::new(); queue.push_back(start); visited.insert(start); while let Some(curr) = queue.pop_front() { for &next in &adj[curr] { if visited.insert(next) { queue.push_back(next); } } } visited }; // 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 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(); for (i, &idx) in vertex_indices.iter().enumerate() { let point = &pts.elements()[idx]; vertices.push(Vertex { id: idx, label: format!("vertex_{}", i), point: format_point(point), coords: layout_coords(point), }); } // Build wires with endpoint connections let mut wires: Vec = Vec::new(); for (i, &idx) in wire_indices.iter().enumerate() { let point = &pts.elements()[idx]; // Find connected VISIBLE vertices via transitive reachability let reachable_up = reachable_from(idx, &successors); let reachable_down = reachable_from(idx, &predecessors); let mut connected: Vec = reachable_up .union(&reachable_down) .filter(|v| vertex_set.contains(v)) .copied() .collect(); connected.sort(); connected.dedup(); let endpoints = if connected.len() >= 2 { [connected[0], connected[1]] } else if connected.len() == 1 { [connected[0], connected[0]] } else { [vertex_indices[0], vertex_indices.get(1).copied().unwrap_or(vertex_indices[0])] }; let endpoint_coords = [ 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: layout_coords(point), endpoints, endpoint_coords, }); } // Build visible surfaces (geom_dim=2) let surface_indices = by_geom_dim.get(&2).map(|v| v.as_slice()).unwrap_or(&[]); 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 // Filter to only VISIBLE wires let mut boundary_wires: Vec = successors[idx] .iter() .chain(predecessors[idx].iter()) .filter(|v| wire_set.contains(v)) .copied() .collect(); boundary_wires.sort(); boundary_wires.dedup(); surfaces.push(Surface { id: idx, label: format!("surface_{}", i), point: format_point(point), coords: layout_coords(point), boundary_wires, }); } // 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, }; // Output JSON let json_output = serde_json::to_string_pretty(&geometry).expect("Failed to serialize"); // Write to file 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 (VISIBLE ONLY)"); eprintln!(" {} vertices", geometry.vertices.len()); eprintln!(" {} wires", geometry.wires.len()); eprintln!(" {} surfaces", geometry.surfaces.len()); // Also print to stdout for piping println!("{}", json_output); }