zigzag-engine/examples/render_braiding.rs
Maximus Gorog c011af0414 Add visibility filter and output only visible elements
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>
2026-04-08 02:16:36 -06:00

287 lines
8.7 KiB
Rust

//! 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<Vertex>,
wires: Vec<Wire>,
surfaces: Vec<Surface>,
}
#[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<usize>,
}
/// 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::<Vec<_>>()
.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<usize> = 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<usize, Vec<usize>> = 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<usize>> = vec![vec![]; pts.len()];
let mut predecessors: Vec<Vec<usize>> = 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<usize>]| -> HashSet<usize> {
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<usize> = 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<usize> = wire_indices.iter().copied().collect();
// Build vertices
let mut vertices: Vec<Vertex> = 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<Wire> = 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<usize> = 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<Surface> = 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<usize> = 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);
}