//! Two levels of occlusion acceleration: an instance tree over projected bounds, and one //! triangle hierarchy per shared mesh. Per-placement node bounds are small; projected //! triangle coordinates are evaluated only in visited leaves. Covered line intervals prune //! subsequent nodes and instances, including the entire query once coverage is complete. use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Instant; use solidcraft_doc::apply_point; use solidcraft_geom::{Aabb3, Mesh, Vec3}; use solidcraft_render::Camera; use super::super::meshes::{Placement, Scene, WORKERS, claim, transformed_bounds}; use super::super::progress::Stage; use super::{Drawing, Line, bounds, clip, key, merge_lines, occluded, overlap, xy}; const TOO_COMPLEX: &str = "view exceeds export work budget; hide bodies or increase tolerance"; const MAX_LINES: usize = 1_000_000; const MAX_SEGMENTS: usize = 4_000_000; const MAX_SHARED_TRIANGLES: usize = 2_000_000; const MAX_VIEW_NODES: usize = 256 * 1024 * 1024 / std::mem::size_of::(); const WORK: usize = 200_000_000; fn parallel(items: &[T], f: impl Fn(&T) -> Result + Sync) -> Result, &'static str> { solidcraft_kernel::map_on_threads(items, WORKERS, f).into_iter().map(|r| r.ok_or("export worker failed")?).collect() } struct Node { bounds: Aabb3, start: usize, end: usize, children: Option<(usize, usize)>, } struct Bvh { nodes: Vec, ids: Vec, } impl Bvh { fn new(boxes: &[Aabb3], flat: bool) -> Self { let mut tree = Self { nodes: Vec::new(), ids: (0..boxes.len()).collect() }; if !boxes.is_empty() { tree.build(boxes, 0, boxes.len(), flat); } tree } fn build(&mut self, boxes: &[Aabb3], start: usize, end: usize, flat: bool) -> usize { let b = self.ids.get(start..end).into_iter().flatten().filter_map(|i| boxes.get(*i)).fold(Aabb3::EMPTY, |b, t| b.union(t)); let index = self.nodes.len(); self.nodes.push(Node { bounds: b, start, end, children: None }); if end - start > 8 { let size = b.size(); let axis = if !flat && size.z > size.x.max(size.y) { 2 } else if size.x > size.y { 0 } else { 1 }; let value = |i: &usize| { boxes.get(*i).map(|b| b.center()).map_or(0., |p| match axis { 0 => p.x, 1 => p.y, _ => p.z, }) }; let mid = start + (end - start) / 2; if let Some(ids) = self.ids.get_mut(start..end) { ids.select_nth_unstable_by(mid - start, |a, b| value(a).total_cmp(&value(b)).then_with(|| a.cmp(b))); } let left = self.build(boxes, start, mid, flat); let right = self.build(boxes, mid, end, flat); if let Some(node) = self.nodes.get_mut(index) { node.children = Some((left, right)); } } index } } struct Edge { vertices: [u32; 2], start: usize, end: usize, } struct Side { key: [[i64; 3]; 2], vertices: [u32; 2], triangle: usize, } struct Shared { mesh: Arc, tree: Bvh, edges: Vec, incident: Vec, } impl Shared { fn new(mesh: &Arc, eps: f64, progress: &Stage) -> Result { let mut boxes = Vec::with_capacity(mesh.triangles.len()); let mut sides = Vec::new(); for (i, indices) in mesh.triangles.iter().enumerate() { if i.is_multiple_of(1024) { progress.check()?; } let points = mesh.tri(indices).ok_or("invalid export triangle")?; if points.iter().any(|p| !p.is_finite()) { return Err("model contains invalid coordinates"); } boxes.push(bounds(points)); for (a, b) in [(0, 1), (1, 2), (2, 0)] { let (Some(pa), Some(pb), Some(ia), Some(ib)) = (points.get(a), points.get(b), indices.get(a), indices.get(b)) else { return Err("invalid export edge"); }; let (ka, kb) = (key(*pa, eps), key(*pb, eps)); if ka == kb { continue; } let (key, vertices) = if ka < kb { ([ka, kb], [*ia, *ib]) } else { ([kb, ka], [*ib, *ia]) }; sides.push(Side { key, vertices, triangle: i }); } } let tree = Bvh::new(&boxes, false); sides.sort_unstable_by(|a, b| a.key.cmp(&b.key).then_with(|| a.triangle.cmp(&b.triangle))); let mut edges: Vec = Vec::new(); let mut incident = Vec::with_capacity(sides.len()); let mut previous = None; for side in sides { if previous != Some(side.key) { edges.push(Edge { vertices: side.vertices, start: incident.len(), end: incident.len() }); previous = Some(side.key); } incident.push(side.triangle); if let Some(e) = edges.last_mut() { e.end = incident.len(); } } Ok(Self { mesh: mesh.clone(), tree, edges, incident }) } } struct View<'a> { cam: &'a Camera, right: Vec3, up: Vec3, back: Vec3, } impl<'a> View<'a> { fn new(cam: &'a Camera) -> Self { let (right, up, back) = cam.basis(); Self { cam, right, up, back } } fn project(&self, p: Vec3) -> Result { if !p.is_finite() { return Err("model contains invalid coordinates"); } let d = p - self.cam.target; let z = d.dot(self.back); let projected = if self.cam.fov > 0. { let w = self.cam.distance - z; if w <= self.cam.distance * 1e-6 { return Err("model crosses the camera eye plane; zoom out before exporting"); } Vec3::new(d.dot(self.right) * self.cam.distance / w, d.dot(self.up) * self.cam.distance / w, 1. / w) } else { Vec3::new(d.dot(self.right), d.dot(self.up), z) }; if !projected.is_finite() || projected.len() > 1e12 { return Err("projected coordinates exceed the export range"); } Ok(projected) } fn triangle(&self, shared: &Shared, placed: &Placement, id: usize) -> Result<[Vec3; 3], &'static str> { let indices = shared.mesh.triangles.get(id).ok_or("invalid export triangle")?; let [a, b, c] = shared.mesh.tri(indices).ok_or("invalid export triangle")?; Ok([ self.project(apply_point(&placed.transform, a))?, self.project(apply_point(&placed.transform, b))?, self.project(apply_point(&placed.transform, c))?, ]) } } struct Projected { nodes: Vec, segments: Vec<(Vec3, Vec3)>, } impl Projected { fn new(shared: &Shared, placed: &Placement, view: &View<'_>, eps: f64, segments: &AtomicUsize, progress: &Stage) -> Result { let mut nodes = vec![Aabb3::EMPTY; shared.tree.nodes.len()]; // Exact projected leaf bounds avoid false near-eye errors from empty corners of a // rotated model-space box. Interior bounds are just unions of the child bounds. let mut front = vec![false; shared.mesh.triangles.len()]; for (i, node) in shared.tree.nodes.iter().enumerate().rev() { if i.is_multiple_of(1024) { progress.check()?; } let mut b = Aabb3::EMPTY; if let Some((left, right)) = node.children { b = nodes.get(left).ok_or("invalid export tree")?.union(nodes.get(right).ok_or("invalid export tree")?); } else { for id in shared.tree.ids.get(node.start..node.end).ok_or("invalid export tree")? { let p = view.triangle(shared, placed, *id)?; b = b.union(&bounds(p)); if let Some(f) = front.get_mut(*id) { // A positive projective scale preserves the projected winding. *f = super::cross(xy(p[1] - p[0]), xy(p[2] - p[0])) > 0.; } } } if let Some(slot) = nodes.get_mut(i) { *slot = b; } } let mut result = Vec::new(); let mut add = |a: Vec3, b: Vec3| -> Result<(), &'static str> { let (a, b) = (view.project(apply_point(&placed.transform, a))?, view.project(apply_point(&placed.transform, b))?); if xy(a).dist(xy(b)) > eps { if !claim(segments, 1, MAX_SEGMENTS) { return Err(TOO_COMPLEX); } result.push((a, b)); } Ok(()) }; for (i, edge) in shared.mesh.edges.iter().enumerate() { if !placed.visibility.get(i).copied().unwrap_or(false) { continue; } for w in edge.windows(2) { if let (Some(a), Some(b)) = (w.first(), w.get(1)) { add(*a, *b)?; } } } for edge in &shared.edges { let mut positive = false; let mut negative = false; for id in shared.incident.get(edge.start..edge.end).ok_or("invalid export adjacency")? { if front.get(*id).copied().ok_or("invalid export adjacency")? { positive = true; } else { negative = true; } } if (positive && negative) || edge.end - edge.start == 1 { let a = shared.mesh.positions.get(edge.vertices[0] as usize).ok_or("invalid silhouette")?; let b = shared.mesh.positions.get(edge.vertices[1] as usize).ok_or("invalid silhouette")?; add(*a, *b)?; } } Ok(Self { nodes, segments: result }) } } /// A sorted union, updated as occluders are found. A node's line interval already covered by /// nearer triangles cannot change the result and is skipped before visiting its children. #[derive(Default)] struct Coverage { spans: Vec<(f64, f64)>, } impl Coverage { fn contains(&self, lo: f64, hi: f64) -> bool { self.spans.iter().any(|(a, b)| *a <= lo && *b >= hi) } fn add(&mut self, mut lo: f64, mut hi: f64) -> Result<(), &'static str> { let first = self.spans.partition_point(|(_, b)| *b + 1e-9 < lo); let mut end = first; while let Some((a, b)) = self.spans.get(end) { if *a > hi + 1e-9 { break; } lo = lo.min(*a); hi = hi.max(*b); end += 1; } if first == end && self.spans.len() >= MAX_LINES / 2 { return Err(TOO_COMPLEX); } self.spans.splice(first..end, [(lo, hi)]); Ok(()) } } fn rectangle(a: Vec3, b: Vec3, box_: &Aabb3) -> Option<(f64, f64)> { let (mut lo, mut hi) = (0., 1.); for (v0, v1) in [ (a.x - box_.min.x, b.x - box_.min.x), (box_.max.x - a.x, box_.max.x - b.x), (a.y - box_.min.y, b.y - box_.min.y), (box_.max.y - a.y, box_.max.y - b.y), ] { if !clip(&mut lo, &mut hi, v0, v1) { return None; } } Some((lo, hi)) } struct Budget<'a> { global: &'a AtomicUsize, progress: &'a Stage, local: usize, spent: usize, } impl<'a> Budget<'a> { fn new(global: &'a AtomicUsize, progress: &'a Stage) -> Self { Self { global, progress, local: 0, spent: 0 } } fn spend(&mut self) -> Result<(), &'static str> { if self.local == 0 { self.progress.check()?; let mut old = self.global.load(Ordering::Relaxed); loop { let granted = old.min(4096); if granted == 0 { return Err(TOO_COMPLEX); } match self.global.compare_exchange_weak(old, old - granted, Ordering::Relaxed, Ordering::Relaxed) { Ok(_) => { self.local = granted; break; } Err(current) => old = current, } } } self.local -= 1; self.spent += 1; Ok(()) } } struct Query<'a, 'b> { shared: &'a [Shared], scene: &'a Scene, projected: &'a [Projected], instances: &'a Bvh, view: &'a View<'a>, depth_eps: f64, a: Vec3, b: Vec3, bounds: Aabb3, covered: Coverage, budget: &'b mut Budget<'a>, } impl Query<'_, '_> { fn relevant(&self, b: &Aabb3) -> bool { if b.max.z <= self.a.z.min(self.b.z) + self.depth_eps || !overlap(b, &self.bounds) { return false; } rectangle(self.a, self.b, b).is_some_and(|(lo, hi)| !self.covered.contains(lo, hi)) } fn instance_node(&mut self, index: usize) -> Result<(), &'static str> { self.budget.spend()?; let node = self.instances.nodes.get(index).ok_or("invalid instance tree")?; if !self.relevant(&node.bounds) { return Ok(()); } if let Some((left, right)) = node.children { let depth = |i: usize| self.instances.nodes.get(i).map_or(f64::NEG_INFINITY, |n| n.bounds.max.z); let (near, far) = if depth(left) >= depth(right) { (left, right) } else { (right, left) }; self.instance_node(near)?; if !self.covered.contains(0., 1.) { self.instance_node(far)?; } } else { for id in self.instances.ids.get(node.start..node.end).ok_or("invalid instance tree")? { self.mesh_node(*id, 0)?; if self.covered.contains(0., 1.) { break; } } } Ok(()) } fn mesh_node(&mut self, instance: usize, index: usize) -> Result<(), &'static str> { self.budget.spend()?; let placed = self.scene.placements.get(instance).ok_or("invalid instance")?; let shared = self.shared.get(placed.mesh).ok_or("invalid shared geometry")?; let projected = self.projected.get(instance).ok_or("invalid instance bounds")?; let b = projected.nodes.get(index).ok_or("invalid mesh tree")?; if !self.relevant(b) { return Ok(()); } let node = shared.tree.nodes.get(index).ok_or("invalid mesh tree")?; if let Some((left, right)) = node.children { let depth = |i: usize| projected.nodes.get(i).map_or(f64::NEG_INFINITY, |b| b.max.z); let (near, far) = if depth(left) >= depth(right) { (left, right) } else { (right, left) }; self.mesh_node(instance, near)?; if !self.covered.contains(0., 1.) { self.mesh_node(instance, far)?; } } else { for id in shared.tree.ids.get(node.start..node.end).ok_or("invalid mesh tree")? { self.budget.spend()?; let points = self.view.triangle(shared, placed, *id)?; if let Some((lo, hi)) = occluded(self.a, self.b, points, self.depth_eps) { self.covered.add(lo, hi)?; if self.covered.contains(0., 1.) { break; } } } } Ok(()) } } pub(super) fn drawing(scene: &Scene, cam: &Camera) -> Result { let start = Instant::now(); let scale = scene .placements .iter() .filter_map(|p| scene.meshes.get(p.mesh).map(|m| transformed_bounds(m, &p.transform))) .fold(Aabb3::EMPTY, |b, p| b.union(&p)) .diagonal() .max(1.); if !scale.is_finite() { return Err("model contains invalid coordinates"); } let eps = scale * 1e-8; // Indexed input can have many triangles but very few positions. Bound adjacency/sort // scratch space as well as the retained mesh bytes before starting parallel builders. let unique_triangles = scene.meshes.iter().try_fold(0usize, |n, m| n.checked_add(m.triangles.len())); if unique_triangles.is_none_or(|n| n > MAX_SHARED_TRIANGLES) { return Err("view exceeds export memory budget; hide bodies or increase tolerance"); } let progress = Stage::new("dxf_index", scene.meshes.len()); let shared = parallel(&scene.meshes, |m| { progress.check()?; let shared = Shared::new(m, eps, &progress)?; progress.advance(1)?; Ok(shared) })?; let count = scene.placements.iter().try_fold(0usize, |n, p| n.checked_add(shared.get(p.mesh)?.tree.nodes.len())); if count.is_none_or(|n| n > MAX_VIEW_NODES) { return Err("view exceeds export memory budget; hide bodies or increase tolerance"); } let view = View::new(cam); let count = AtomicUsize::new(0); let progress = Stage::new("dxf_projection", scene.placements.len()); let projected = parallel(&scene.placements, |p| { progress.check()?; let projected = Projected::new(shared.get(p.mesh).ok_or("missing shared geometry")?, p, &view, eps, &count, &progress)?; progress.advance(1)?; Ok(projected) })?; let boxes = projected.iter().map(|p| p.nodes.first().copied().unwrap_or(Aabb3::EMPTY)).collect::>(); let mut instances = Bvh::new(&boxes, true); // The view is fixed for the entire drawing. Sort the at-most-eight members of each // instance leaf once, rather than allocating/sorting on every line query. for node in instances.nodes.iter().filter(|n| n.children.is_none()) { if let Some(ids) = instances.ids.get_mut(node.start..node.end) { ids.sort_unstable_by(|a, b| { boxes .get(*b) .map_or(f64::NEG_INFINITY, |b| b.max.z) .total_cmp(&boxes.get(*a).map_or(f64::NEG_INFINITY, |b| b.max.z)) .then_with(|| a.cmp(b)) }); } } let segment_count = count.load(Ordering::Relaxed); let projection_ms = start.elapsed().as_secs_f64() * 1000.; let start = Instant::now(); let depth_eps = if cam.fov > 0. { 1e-8 / cam.distance } else { eps }; let global = AtomicUsize::new(WORK); let output = AtomicUsize::new(0); let chunks = projected.iter().flat_map(|p| p.segments.chunks(128)).collect::>(); let progress = Stage::new("dxf_occlusion", segment_count); let results = parallel(&chunks, |chunk| { progress.check()?; let mut budget = Budget::new(&global, &progress); let mut lines = Vec::new(); for &(a, b) in *chunk { let mut query = Query { shared: &shared, scene, projected: &projected, instances: &instances, view: &view, depth_eps, a, b, bounds: bounds([a, b]), covered: Coverage::default(), budget: &mut budget, }; query.instance_node(0)?; let mut cursor = 0.; let mut emit = |lo: f64, hi: f64, hidden: bool| -> Result<(), &'static str> { let (p, q) = (xy(a + (b - a) * lo), xy(a + (b - a) * hi)); if p.dist(q) > eps { if !claim(&output, 1, MAX_LINES) { return Err(TOO_COMPLEX); } lines.push(Line { a: p, b: q, hidden }); } Ok(()) }; for (lo, hi) in query.covered.spans { emit(cursor, lo, false)?; emit(lo, hi, true)?; cursor = hi; } emit(cursor, 1., false)?; } progress.advance(chunk.len())?; Ok((lines, budget.spent)) })?; let work = results.iter().map(|(_, n)| *n).sum(); let lines = merge_lines(results.into_iter().flat_map(|(lines, _)| lines).collect(), eps); progress.check()?; Ok(Drawing { lines, work, segments: segment_count, projection_ms, occlusion_ms: start.elapsed().as_secs_f64() * 1000. }) } #[cfg(test)] mod tests { use super::*; #[test] fn coverage_handles_disjoint_spans_and_early_complete_coverage() { let mut c = Coverage::default(); c.add(0.4, 0.6).unwrap(); c.add(0., 0.2).unwrap(); assert!(c.contains(0.05, 0.15)); assert!(!c.contains(0., 1.)); c.add(0.2, 0.9).unwrap(); c.add(0.8, 1.).unwrap(); assert_eq!(c.spans, vec![(0., 1.)]); } fn box_mesh() -> Mesh { let mut mesh = Mesh::default(); mesh.positions = vec![ Vec3::new(0., 0., 0.), Vec3::new(8., 0., 0.), Vec3::new(8., 6., 0.), Vec3::new(0., 6., 0.), Vec3::new(0., 0., 3.), Vec3::new(8., 0., 3.), Vec3::new(8., 6., 3.), Vec3::new(0., 6., 3.), ]; mesh.triangles = vec![[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7], [0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]]; mesh.edges = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)] .into_iter() .map(|(a, b)| vec![mesh.positions[a], mesh.positions[b]]) .collect(); mesh } fn canonical(lines: Vec) -> Vec<([i64; 2], [i64; 2], bool)> { let k = |p: solidcraft_geom::Vec2| [(p.x * 1e6).round() as i64, (p.y * 1e6).round() as i64]; let mut result = lines .into_iter() .map(|l| { let (a, b) = (k(l.a), k(l.b)); if a < b { (a, b, l.hidden) } else { (b, a, l.hidden) } }) .collect::>(); result.sort_unstable(); result } #[test] fn shared_rotated_instances_match_flat_occlusion_in_orthographic_and_perspective_views() { use std::sync::Arc; let mesh = box_mesh(); let mut placements = Vec::new(); let mut flat = Vec::new(); for (offset, angle) in [(Vec3::ZERO, 0.), (Vec3::new(3., 2., 4.), 0.3), (Vec3::new(2., 1., -4.), -0.6)] { let transform = solidcraft_doc::rigid(offset, Vec3::ZERO, Vec3::Z, angle); placements.push(Placement { mesh: 0, transform, visibility: vec![true; mesh.edges.len()] }); let mut placed = mesh.clone(); placed.positions = placed.positions.iter().map(|p| apply_point(&transform, *p)).collect(); placed.edges = placed.edges.iter().map(|e| e.iter().map(|p| apply_point(&transform, *p)).collect()).collect(); flat.push(placed); } let scene = Scene { meshes: vec![Arc::new(mesh)], placements, triangles: 36, bytes: 0 }; for (yaw, pitch, roll) in [(0., std::f64::consts::FRAC_PI_2, 0.), (0.7, 0.4, 0.2), (0., 0., 0.)] { for fov in [0., 0.6] { let cam = Camera { target: Vec3::new(4., 3., 1.), yaw, pitch, roll, fov, distance: 100., ..Default::default() }; let expected = super::super::reference_drawing(&flat, &cam).unwrap(); let actual = drawing(&scene, &cam).unwrap(); assert_eq!(canonical(actual.lines), canonical(expected), "yaw={yaw}, pitch={pitch}, fov={fov}"); } } } #[test] fn repeated_surface_over_one_million_triangles_keeps_one_mesh_and_exports_all_placements() { use solidcraft_render::StandardView; use std::sync::Arc; let mut mesh = Mesh::default(); for y in 0..=100 { for x in 0..=100 { mesh.positions.push(Vec3::new(x as f64 / 10., y as f64 / 10., 0.)); } } for y in 0..100u32 { for x in 0..100u32 { let a = y * 101 + x; mesh.triangles.extend([[a, a + 1, a + 102], [a, a + 102, a + 101]]); } } mesh.edges = vec![ vec![Vec3::ZERO, Vec3::new(10., 0., 0.)], vec![Vec3::new(10., 0., 0.), Vec3::new(10., 10., 0.)], vec![Vec3::new(10., 10., 0.), Vec3::new(0., 10., 0.)], vec![Vec3::new(0., 10., 0.), Vec3::ZERO], ]; let placements = (0..60) .map(|i| Placement { mesh: 0, transform: solidcraft_doc::rigid(Vec3::new((i % 10) as f64 * 20., (i / 10) as f64 * 20., 0.), Vec3::ZERO, Vec3::Z, 0.), visibility: vec![true; 4], }) .collect(); let scene = Scene { meshes: vec![Arc::new(mesh)], placements, triangles: 1_200_000, bytes: 0 }; let mut cam = Camera { target: Vec3::new(95., 55., 0.), fov: 0., distance: 300., ..Default::default() }; cam.set_view(StandardView::Top); let result = drawing(&scene, &cam).unwrap(); assert_eq!(scene.meshes.len(), 1); assert_eq!(result.lines.len(), 240); assert!(result.lines.iter().all(|l| !l.hidden)); } }