From d5cb2769651bbb4d220a64ecc2059a53dd5e726c Mon Sep 17 00:00:00 2001 From: Iraklis Kourtis Date: Sun, 11 Oct 2026 17:57:07 -0400 Subject: [PATCH] feat(kernel): every boolean path reports exact face and edge history ops::boolean's result carries its history over [a, b] whichever path made it, recorded by the code that builds the faces: - truck path (incl. jitter retries): shapeops history mapped through the order-preserving copies and the cut's turned tool, then through healing - heal records which input faces each healed face merges and which input edges each joined or exact edge replaces (HealMap); opt-in, so heal_keep callers pay nothing - trivial shortcuts, the void, glue, faces pushed apart (push_faces records its own history; the slab cut back on a union maps to the operands' faces in that plane), the nudged retry, by_lumps and join composition - prism booleans: section segments carry tags (operand cap edge, the wall beside it, its vertices) through the 2D boolean; walls, caps and edges of the extruded or sewn result follow from them - planar and faceted polygon booleans: polygons carry their operand face; faces join the pieces across edges, edges come from the pieces either side History gains renumber/over (and consuming forms) for composition; record gains regions/traced/build helpers. truck-shapeops *_with_history now return the solid with an Option history. Paths that cannot report (bore narrowing via offset, inexpressible lineage) leave the result without history. Boolean results, tolerances and fallback order are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8 --- crates/kernel/src/body.rs | 15 + crates/kernel/src/boolean_history_tests.rs | 404 +++++++++++ crates/kernel/src/boolean_record.rs | 170 +++++ crates/kernel/src/coplanar.rs | 101 ++- crates/kernel/src/heal.rs | 165 ++++- crates/kernel/src/history.rs | 69 ++ crates/kernel/src/lib.rs | 1 + crates/kernel/src/ops.rs | 160 +++-- crates/kernel/src/polybool.rs | 240 +++++-- crates/kernel/src/prism.rs | 640 +++++++++++++----- crates/kernel/src/prism_lineage.rs | 380 +++++++++++ crates/kernel/src/record.rs | 97 ++- crates/kernel/src/sew.rs | 13 +- vendor/README.md | 3 +- .../src/transversal/integrate/mod.rs | 20 +- .../src/transversal/integrate/tests.rs | 3 + 16 files changed, 2169 insertions(+), 312 deletions(-) create mode 100644 crates/kernel/src/boolean_history_tests.rs create mode 100644 crates/kernel/src/boolean_record.rs create mode 100644 crates/kernel/src/prism_lineage.rs diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index 2016891e..c24450ac 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -465,6 +465,21 @@ impl Body { Ok(self) } + /// This body with `history` if it describes the body made from operands with these face + /// and edge counts, else with none: an operation whose history could not be recorded still + /// gives its result. + pub(crate) fn with_checked_history(mut self, history: Option, operands: &[(usize, usize)]) -> Self { + let counts = (self.face_count(), self.edge_count()); + self.history = history.and_then(|h| match h.check_counts(counts, operands) { + Ok(()) => Some(std::sync::Arc::new(h)), + Err(e) => { + log::debug!("no history: {e}"); + None + } + }); + self + } + pub fn face_sources(&self) -> Option<&[Option]> { self.face_sources.as_deref().map(Vec::as_slice) } diff --git a/crates/kernel/src/boolean_history_tests.rs b/crates/kernel/src/boolean_history_tests.rs new file mode 100644 index 00000000..958328e1 --- /dev/null +++ b/crates/kernel/src/boolean_history_tests.rs @@ -0,0 +1,404 @@ +//! Booleans' histories, path by path: every face and edge origin checked against the geometry +//! of the operands (in tests only: the history itself is recorded, never sampled), and the exact +//! origins asserted where the outcome is known. + +use std::collections::BTreeSet; + +use solvecraft_geom::{Mesh, Vec3}; + +use crate::body::Body; +use crate::history::{Change, EdgeOrigin, FaceOrigin, History}; +use crate::{BoolOp, boolean, box_solid}; + +const fn face(operand: usize, face: usize) -> FaceOrigin { + FaceOrigin::Face { operand, face } +} + +fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 { + // Ericson, Real-Time Collision Detection, 5.1.5. + let (ab, ac, ap) = (b - a, c - a, p - a); + let (d1, d2) = (ab.dot(ap), ac.dot(ap)); + if d1 <= 0.0 && d2 <= 0.0 { + return a; + } + let bp = p - b; + let (d3, d4) = (ab.dot(bp), ac.dot(bp)); + if d3 >= 0.0 && d4 <= d3 { + return b; + } + let vc = d1 * d4 - d3 * d2; + if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 { + return a + ab * (d1 / (d1 - d3)); + } + let cp = p - c; + let (d5, d6) = (ab.dot(cp), ac.dot(cp)); + if d6 >= 0.0 && d5 <= d6 { + return c; + } + let vb = d5 * d2 - d1 * d6; + if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 { + return a + ac * (d2 / (d2 - d6)); + } + let va = d3 * d6 - d5 * d4; + if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 { + return b + (c - b) * ((d4 - d3) / ((d4 - d3) + (d5 - d6))); + } + let denom = 1.0 / (va + vb + vc); + a + ab * (vb * denom) + ac * (vc * denom) +} + +/// Distance from `p` to face `f` of a mesh. +fn to_face(m: &Mesh, f: usize, p: Vec3) -> f64 { + m.triangles + .iter() + .zip(&m.tri_face) + .filter(|(_, x)| **x as usize == f) + .map(|(t, _)| { + let [a, b, c] = t.map(|i| m.positions[i as usize]); + closest_on_triangle(p, a, b, c).dist(p) + }) + .fold(f64::INFINITY, f64::min) +} + +/// Distance from `p` to edge `e` of a mesh. +fn to_edge(m: &Mesh, e: usize, p: Vec3) -> f64 { + m.edges[e].windows(2).map(|w| p.dist_to_segment(w[0], w[1])).fold(f64::INFINITY, f64::min) +} + +/// The result's history, checked against it and its operands, with every origin checked +/// against the geometry: each face's triangles lie on its origin faces, each origin face holds +/// some of them; each edge lies along its origin edges, or on both faces of a meeting. `tol`: +/// the meshes' tolerance. +fn verified<'a>(r: &'a Body, operands: &[&Body], tol: f64) -> &'a History { + let h = r.history().expect("the boolean reports its history"); + h.check(r, operands).unwrap(); + let rm = r.tessellate(tol).unwrap(); + let ms: Vec = operands.iter().map(|b| b.tessellate(tol).unwrap()).collect(); + let near = 4.0 * tol + r.size() * 1e-6; + let on = |o: &FaceOrigin, p: Vec3| match *o { + FaceOrigin::Face { operand, face } => to_face(&ms[operand], face, p) < near, + _ => false, + }; + for (i, f) in h.faces.iter().enumerate() { + assert!(!f.origins.is_empty(), "face {i} has no origin"); + let centres: Vec = rm + .triangles + .iter() + .zip(&rm.tri_face) + .filter(|(_, x)| **x as usize == i) + .map(|(t, _)| t.map(|k| rm.positions[k as usize]).iter().fold(Vec3::ZERO, |s, p| s + *p) * (1.0 / 3.0)) + .collect(); + for c in ¢res { + assert!(f.origins.iter().any(|o| on(o, *c)), "face {i} ({f:?}) at {c:?} lies on none of its origins"); + } + for o in &f.origins { + assert!(centres.iter().any(|c| on(o, *c)), "face {i}: origin {o:?} is under none of it"); + } + } + for (j, e) in h.edges.iter().enumerate() { + let pts = &rm.edges[j]; + for o in &e.origins { + let ok = |p: &Vec3| match *o { + EdgeOrigin::Edge { operand, edge } => to_edge(&ms[operand], edge, *p) < near, + EdgeOrigin::Meet { a, b } => on(&a, *p) && on(&b, *p), + }; + match o { + // A joined edge runs along all of its origins, each along part of it. + EdgeOrigin::Edge { .. } if e.origins.len() > 1 => assert!(pts.iter().any(ok), "edge {j}: {o:?} is not along it"), + _ => assert!(pts.iter().all(ok), "edge {j} ({e:?}) does not lie along {o:?}"), + } + } + if e.origins.len() > 1 { + for p in pts { + assert!( + e.origins.iter().any(|o| match *o { + EdgeOrigin::Edge { operand, edge } => to_edge(&ms[operand], edge, *p) < near, + EdgeOrigin::Meet { a, b } => on(&a, *p) && on(&b, *p), + }), + "edge {j}: {p:?} lies along none of {e:?}" + ); + } + } + } + h +} + +/// How many faces of each change. +fn changes(h: &History) -> [usize; 5] { + let mut n = [0; 5]; + for f in &h.faces { + n[match f.change { + Change::Kept => 0, + Change::Modified => 1, + Change::Split => 2, + Change::Merged => 3, + Change::Generated => 4, + }] += 1; + } + n +} + +/// The faces of the result that come from operand face `o`. +fn from(h: &History, o: FaceOrigin) -> Vec { + (0..h.faces.len()).filter(|i| h.faces[*i].origins.contains(&o)).collect() +} + +/// The meetings among the edges' origins, as face pairs. +fn meets(h: &History) -> BTreeSet<(FaceOrigin, FaceOrigin)> { + h.edges.iter().flat_map(|e| &e.origins).filter_map(|o| if let EdgeOrigin::Meet { a, b } = o { Some((*a, *b)) } else { None }).collect() +} + +fn boxed(x0: f64, y0: f64, z0: f64, x1: f64, y1: f64, z1: f64) -> Body { + box_solid(Vec3::new(x0, y0, z0), Vec3::new(x1, y1, z1)).unwrap() +} + +/// A short summary of a history for failure messages. +fn show(h: &History) -> String { + let faces: Vec = h.faces.iter().enumerate().map(|(i, f)| format!("{i}: {:?} {:?}", f.change, f.origins)).collect(); + let edges: Vec = h.edges.iter().enumerate().map(|(i, e)| format!("{i}: {:?} {:?}", e.faces, e.origins)).collect(); + format!("faces\n{}\nedges\n{}", faces.join("\n"), edges.join("\n")) +} + +/// Each face's origins and change, in face order. +fn faces_of(h: &History) -> Vec<(Change, Vec)> { + h.faces.iter().map(|f| (f.change, f.origins.clone())).collect() +} + +/// Truck's boolean with a sphere (no prism, no coincident faces): a box with a corner taken +/// off by a sphere. The three faces at the corner get a notch, the sphere's patches inside the +/// box bound it, meeting the box's faces along new edges. +#[test] +fn box_minus_sphere_through_truck() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = crate::sphere(Vec3::new(10.0, 10.0, 10.0), 4.0).unwrap(); + let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap(); + let h = verified(&r, &[&a, &b], 0.02); + assert_eq!(changes(h), [3, 6, 0, 0, 0], "{}", show(h)); + let from_b: BTreeSet = h + .faces + .iter() + .flat_map(|f| &f.origins) + .filter_map(|o| match o { + FaceOrigin::Face { operand: 1, face } => Some(*face), + _ => None, + }) + .collect(); + assert_eq!(from_b.len(), 3); + // Each notched box face meets two of the sphere's patches. + assert_eq!(meets(h).len(), 6, "{}", show(h)); + assert!(meets(h).iter().all(|(x, y)| matches!((x, y), (FaceOrigin::Face { operand: 0, .. }, FaceOrigin::Face { operand: 1, .. })))); +} + +/// The jitter retries move both operands and the result back: the history holds for the +/// operands themselves, the same faces coming from the same operand faces. +#[test] +fn shifted_truck_attempt_keeps_the_history() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = crate::sphere(Vec3::new(10.0, 10.0, 10.0), 4.0).unwrap(); + let counts = crate::record::counts(&[&a, &b]); + let mut compared = 0; + for op in [BoolOp::Cut, BoolOp::Union, BoolOp::Intersect] { + let Ok((_, Some((plain, _)))) = crate::ops::truck_boolean(&a, &b, op, None, 0.035) else { continue }; + // The shifts the retries use (a hundredth of the size along each). + for j in [[0.1373, 0.2719, 0.3911], [-0.5127, 0.0833, 0.2291], [0.6911, -0.3337, -0.6127]] { + let shift = truck_modeling::Vector3::new(j[0], j[1], j[2]) * 0.17; + let Ok((s, h)) = crate::ops::truck_boolean(&a, &b, op, Some(shift), 0.035) else { continue }; + let faces = h.as_ref().map(|(f, _)| f.clone()); + assert_eq!(faces, Some(plain.clone()), "{op:?}"); + let r = crate::boolean_record::truck_result(s, h, 17.4, &[], &counts).unwrap(); + verified(&r, &[&a, &b], 0.02); + compared += 1; + } + } + assert!(compared > 0); +} + +/// Two prisms along x, the tool's section inside the target's, overlapping in part: sewn slab by +/// slab, then healed. The target's face at x = 10 gets the tool's outline as a hole, which the +/// tool's sides meet; the tool's sides and far end stand out of it. +#[test] +fn box_union_box_prism() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = boxed(5.0, 2.0, 2.0, 15.0, 8.0, 8.0); + let r = boolean(&a, &b, BoolOp::Union).unwrap().unwrap(); + let h = verified(&r, &[&a, &b], 0.02); + assert_eq!(h.faces.len(), 11); + let mut origins: Vec = h + .faces + .iter() + .map(|f| match f.origins.as_slice() { + [o] => *o, + _ => panic!("one origin each: {}", show(h)), + }) + .collect(); + origins.sort(); + // All of a's faces, and b's but the end inside a. + let mut want: Vec = (0..6).map(|i| face(0, i)).chain((0..6).map(|i| face(1, i))).collect(); + let inside = face(1, 4); + want.retain(|o| *o != inside); + assert_eq!(origins, want, "{}", show(h)); + // b's four sides meet a's face at x = 10. + let rim: BTreeSet<_> = meets(h).iter().map(|(x, _)| *x).collect(); + assert_eq!(meets(h).len(), 4); + assert_eq!(rim.len(), 1); + assert!(h.edges.iter().all(|e| e.origins.len() == 1)); +} + +/// A through hole: the box's sides are kept, top and bottom get the hole, the cylinder's two +/// half walls line it, meeting top and bottom along its rims, and meet each other along the +/// cylinder's own seams. +#[test] +fn box_minus_cylinder_prism() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = crate::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 12.0).unwrap(); + let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap(); + let h = verified(&r, &[&a, &b], 0.01); + assert_eq!(changes(h), [4, 4, 0, 0, 0], "{}", show(h)); + let walls = from(h, face(1, 1)).len() + from(h, face(1, 2)).len(); + assert_eq!(walls, 2, "{}", show(h)); + // Rims: each wall meets top and bottom. + assert_eq!(meets(h).len(), 4, "{}", show(h)); + let seams = h.edges.iter().filter(|e| matches!(e.origins.as_slice(), [EdgeOrigin::Edge { operand: 1, .. }])).count(); + assert_eq!(seams, 2); + // Every other edge is one of the box's twelve. + let boxed_edges: BTreeSet<_> = h.edges.iter().flat_map(|e| &e.origins).filter(|o| matches!(o, EdgeOrigin::Edge { operand: 0, .. })).collect(); + assert_eq!(boxed_edges.len(), 12); +} + +/// A slot across the top (a stepped prism along y): the top and the box's ends come apart in +/// pieces (Split); the slot's floor and sides are the tool's. +#[test] +#[ignore = "fails until #136 is fixed"] +fn slot_splits_a_face() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = boxed(4.0, -1.0, 5.0, 6.0, 11.0, 11.0); + let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap(); + let h = verified(&r, &[&a, &b], 0.02); + let top = (0..6).find(|i| a.planar_face(*i).is_some_and(|p| p.origin.z > 9.0 && p.normal().z > 0.9)).unwrap(); + let pieces = from(h, face(0, top)); + assert!(pieces.len() >= 2, "{}", show(h)); + assert!(pieces.iter().all(|i| h.faces[*i].change == Change::Split)); + // Three of the tool's faces bound the slot. + let tool: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 1, .. })).collect(); + assert_eq!(tool.len(), 3, "{}", show(h)); +} + +/// Bodies clear of each other: a union keeps both whole, a cut keeps the target. +#[test] +fn trivial_paths() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = crate::sphere(Vec3::new(30.0, 0.0, 0.0), 3.0).unwrap(); + let u = boolean(&a, &b, BoolOp::Union).unwrap().unwrap(); + let h = verified(&u, &[&a, &b], 0.02); + let want: Vec<(Change, Vec)> = + (0..a.face_count()).map(|i| (Change::Kept, vec![face(0, i)])).chain((0..b.face_count()).map(|i| (Change::Kept, vec![face(1, i)]))).collect(); + assert_eq!(faces_of(h), want); + let c = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap(); + let h = verified(&c, &[&a, &b], 0.02); + assert_eq!(faces_of(h), (0..a.face_count()).map(|i| (Change::Kept, vec![face(0, i)])).collect::>()); + // Wholly inside: the intersection is the tool, the union the target. + let inner = crate::sphere(Vec3::new(5.0, 5.0, 5.0), 2.0).unwrap(); + let i = boolean(&a, &inner, BoolOp::Intersect).unwrap().unwrap(); + let h = verified(&i, &[&a, &inner], 0.02); + assert!(h.faces.iter().all(|f| f.change == Change::Kept && matches!(f.origins.as_slice(), [FaceOrigin::Face { operand: 1, .. }]))); + let u = boolean(&a, &inner, BoolOp::Union).unwrap().unwrap(); + let h = verified(&u, &[&a, &inner], 0.02); + assert!(h.faces.iter().all(|f| matches!(f.origins.as_slice(), [FaceOrigin::Face { operand: 0, .. }]))); + // Cut out of the middle: a void, the tool's faces turned inward. + let v = boolean(&a, &inner, BoolOp::Cut).unwrap().unwrap(); + let h = verified(&v, &[&a, &inner], 0.02); + assert_eq!(v.shell_count(), 2); + assert_eq!(h.faces.len(), a.face_count() + inner.face_count()); + assert!(h.faces.iter().all(|f| f.change == Change::Kept)); +} + +/// A body in two pieces, cut a piece at a time: each piece's faces are the body's own. +#[test] +fn by_lumps_composes_the_pieces() { + let left = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let right = boxed(20.0, 0.0, 0.0, 30.0, 10.0, 10.0); + let a = boolean(&left, &right, BoolOp::Union).unwrap().unwrap(); + assert_eq!(a.shell_count(), 2); + let b = crate::sphere(Vec3::new(15.0, 5.0, 10.0), 7.0).unwrap(); + let r = crate::ops::by_lumps(&a, &b, a.lumps().unwrap(), b.lumps().unwrap(), BoolOp::Cut).unwrap().unwrap(); + let h = verified(&r, &[&a, &b], 0.02); + // Both pieces are bitten: the sphere's patches inside each line its bite. + let bites: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 1, .. })).collect(); + assert!(bites.len() >= 2, "{}", show(h)); + let targets: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 0, .. })).collect(); + assert_eq!(targets.len(), 12); +} + +/// Two bodies touching along one whole face, back to back: glued, the two faces gone, the +/// coplanar sides each side of the seam merged. +#[test] +fn glue_merges_across_the_seam() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = boxed(10.0, 0.0, 0.0, 20.0, 10.0, 10.0); + let r = crate::coplanar::glue(&a, &b).unwrap().unwrap(); + let h = verified(&r, &[&a, &b], 0.02); + assert_eq!(changes(h), [2, 0, 0, 4, 0], "{}", show(h)); + // The four seam edges went with the merged faces; the ends' edges are each box's. + assert_eq!(h.edges.len(), 12); + assert!(h.edges.iter().all(|e| !e.origins.is_empty())); +} + +/// Coincident faces pushed apart: a cut flush with the target's top, and a union of bodies +/// standing on one floor (the floor that is cut back is both operands' floors). +#[test] +#[ignore = "fails until #136 is fixed"] +fn faces_pushed_apart() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let tool = boxed(2.0, 2.0, 5.0, 8.0, 8.0, 10.0); + let r = crate::coplanar::boolean_apart(&a, &tool, BoolOp::Cut).unwrap().unwrap().unwrap(); + let h = verified(&r, &[&a, &tool], 0.02); + // The pocket: its floor and four sides are the tool's. + let tool_faces: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 1, .. })).collect(); + assert_eq!(tool_faces.len(), 5, "{}", show(h)); + let left = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let right = boxed(5.0, 3.0, 0.0, 15.0, 7.0, 4.0); + let r = crate::coplanar::boolean_apart(&left, &right, BoolOp::Union).unwrap().unwrap().unwrap(); + let h = verified(&r, &[&left, &right], 0.02); + assert!(h.faces.iter().any(|f| f.change == Change::Merged && f.origins.len() == 2), "{}", show(h)); +} + +/// The exact polygon boolean: pieces of planar faces joined back into faces. +#[test] +fn planar_fallback() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = boxed(5.0, 5.0, 5.0, 15.0, 15.0, 15.0); + for (op, count) in [(BoolOp::Cut, 9), (BoolOp::Union, 12), (BoolOp::Intersect, 6)] { + let r = crate::polybool::planar_boolean(&a, &b, op).unwrap().unwrap(); + let h = verified(&r, &[&a, &b], 0.02); + assert_eq!(h.faces.len(), count, "{op:?}\n{}", show(h)); + assert!(h.faces.iter().all(|f| f.origins.len() == 1)); + // Three faces of each meet three of the other's. + assert_eq!(meets(h).len(), 6, "{op:?}\n{}", show(h)); + assert!(h.edges.iter().all(|e| e.origins.len() == 1), "{op:?}\n{}", show(h)); + } +} + +/// The nudged retry: a boolean with the tool moved a hair, composed with the move. +#[test] +fn nudged_tool_composes() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let b = crate::sphere(Vec3::new(10.0, 10.0, 10.0), 4.0).unwrap(); + let nb = crate::transform(&b, Vec3::new(1e-5, 0.0, 0.0), Vec3::ZERO, Vec3::Z, 0.0).unwrap(); + let r = boolean(&a, &nb, BoolOp::Cut).unwrap().unwrap(); + let r = crate::ops::nudged(r, &nb, &crate::record::counts(&[&a, &b])); + verified(&r, &[&a, &b], 0.02); +} + +/// A boolean with a triangle mesh: the mesh's faces are its triangles (and it has no edges). +#[test] +fn faceted_tool() { + let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); + let tool = boxed(5.0, 5.0, 5.0, 15.0, 15.0, 15.0).tessellate(0.1).unwrap(); + let r = crate::faceted_boolean(&a, &tool, BoolOp::Cut, 0.01).unwrap().unwrap(); + let h = r.history().expect("the faceted boolean reports its history"); + h.check_counts((r.face_count(), r.edge_count()), &[(a.face_count(), a.edge_count()), (tool.triangles.len(), 0)]).unwrap(); + // The box's six faces (three notched) and the three tool faces lining the notch. + let from_a: BTreeSet<_> = h.faces.iter().flat_map(|f| &f.origins).filter(|o| matches!(o, FaceOrigin::Face { operand: 0, .. })).collect(); + assert_eq!(from_a.len(), 6); + assert_eq!(h.faces.iter().filter(|f| matches!(f.origins.first(), Some(FaceOrigin::Face { operand: 1, .. }))).count(), 3); +} diff --git a/crates/kernel/src/boolean_record.rs b/crates/kernel/src/boolean_record.rs new file mode 100644 index 00000000..d1ca1301 --- /dev/null +++ b/crates/kernel/src/boolean_record.rs @@ -0,0 +1,170 @@ +//! Recording the exact history of a boolean's result, whichever path made it (see +//! [`crate::ops::boolean`]): for each result face, the operand faces it is a region of; for each +//! result edge, the operand edges it lies along or the operand faces whose meeting it is. The +//! operands are `[a, b]`: the target, then the tool. Paths compose the histories of the steps +//! they take ([`History::over`], [`History::substitute`]); a path that cannot leaves the result +//! without history. + +use std::collections::HashMap; + +use solvecraft_geom::Vec3; +use truck_modeling as mt; + +use crate::Result; +use crate::body::{Body, Solid, edge_order}; +use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv, History}; +use crate::record; + +/// A composition step's history, or none (logged) where the lineage can't be expressed. +pub(crate) fn composed(h: Result) -> Option { + h.map_err(|e| log::debug!("no history: {e}")).ok() +} + +/// `h`, the history of an operation over one operand, as over `operands` with that one at +/// `operand`. +pub(crate) fn placed(h: Option, operand: usize, operands: usize) -> Option { + composed(h?.renumbered(&[Some(operand)], operands)) +} + +/// The history of `body` as operand `operand` of `operands`: every face and edge kept. +pub(crate) fn kept(body: &Body, operand: usize, operands: usize) -> Option { + placed(Some(History::identity(body)), operand, operands) +} + +/// The history of `result`, made from operands of which operand `k` was made by the operation +/// `inner` describes, with `inner`'s operands in `k`'s place. +pub(crate) fn through(result: &Body, k: usize, inner: Option<&History>) -> Option { + composed(result.history()?.clone().substituted(k, inner?)) +} + +/// Operand `operand` (with `counts` giving every operand's faces and edges) as the result, with +/// the history that says so. +pub(crate) fn operand(body: &Body, operand: usize, counts: &[(usize, usize)]) -> Body { + body.clone().with_checked_history(kept(body, operand, counts.len()), counts) +} + +/// The histories of `parts`, each made of shells of `whole` (its lumps), over `[whole]`: their +/// faces and edges are the whole's own (by identity). +pub(crate) fn parts_of(parts: &[Body], whole: &Body) -> Vec> { + let (faces, edges) = (record::face_index(&whole.solid), record::edge_origins(&whole.solid, 0)); + parts + .iter() + .map(|part| { + let faces = part.solid.face_iter().map(|f| Some(record::from_face(0, *faces.get(&f.id())?, Change::Kept))).collect::>>()?; + Some(History { operands: 1, faces, edges: record::edge_provs(part, &edges)? }) + }) + .collect() +} + +/// A body with its history over the operation's operands (`None`: not known). +pub(crate) type Traced = (Body, Option); + +/// The history over the operation's operands of `result`, made from `x` and `y` (in that order) +/// whose histories over them are given. +pub(crate) fn of_pair(result: &Body, x: &Option, y: &Option) -> Option { + composed(result.history()?.clone().over(&[x.as_ref()?, y.as_ref()?])) +} + +/// Bodies apart from each other, each with its history over the operation's operands (with +/// `counts` faces and edges), as one body of several pieces whose history is theirs side by side. +pub(crate) fn join(pieces: Vec, counts: &[(usize, usize)]) -> Result> { + if pieces.len() < 2 { + return Ok(pieces.into_iter().next().map(|(b, h)| b.with_checked_history(h, counts))); + } + // The pieces' shells in order: their faces in order, their edges themselves. + let mut faces = Some(Vec::new()); + let mut edges = HashMap::new(); + for (p, h) in &pieces { + let (Some(h), Some(all)) = (h, faces.as_mut()) else { + faces = None; + break; + }; + all.extend(h.faces.iter().cloned()); + for (e, prov) in edge_order(p.solid.face_iter()).0.iter().zip(&h.edges) { + edges.insert(e.id(), prov.origins.clone()); + } + } + if let Some(faces) = faces.as_mut() { + record::mark_shared_split(faces); + } + let joined = crate::ops::join_pieces(pieces.into_iter().map(|(p, _)| p).collect())?; + Ok(joined.map(|body| record::traced(body, counts, faces, Some(&edges)))) +} + +/// A truck boolean's history in our terms, in the order of the solid it made: each face a +/// region of one operand face (Kept when that very face), each edge's origins. +pub(crate) type Shapeops = (Vec, Vec>); + +/// A truck boolean's history in our terms ([`Shapeops`]). `tool` is the +/// tool as passed (after a cut turned it inside out, which keeps its faces' order but walks each +/// face's edges the other way, so its edge order changes) and `tool_edges` its edges by identity +/// in its own order (before turning). +pub(crate) fn shapeops(h: &truck_shapeops::History, tool: &Solid, tool_edges: Option<&HashMap>>) -> Option { + let face = |f: truck_shapeops::FaceIndex| FaceOrigin::Face { operand: f.operand, face: f.face }; + let faces = + h.faces.iter().map(|o| record::from_face(o.source.operand, o.source.face, if o.kept { Change::Kept } else { Change::Modified })).collect(); + let passed: Vec = edge_order(tool.face_iter()).0.iter().map(|e| e.id()).collect(); + let edges = h + .edges + .iter() + .map(|o| match *o { + truck_shapeops::EdgeOrigin::Edge { operand: 1, edge } if tool_edges.is_some() => { + tool_edges.and_then(|t| t.get(passed.get(edge)?)).cloned() + } + truck_shapeops::EdgeOrigin::Edge { operand, edge } => Some(vec![EdgeOrigin::Edge { operand, edge }]), + truck_shapeops::EdgeOrigin::Meet { a, b } => Some(vec![EdgeOrigin::Meet { a: face(a), b: face(b) }]), + }) + .collect::>>()?; + Some((faces, edges)) +} + +/// The body made of `solid` healed (keeping faces apart along `keep`), with the history whose +/// faces and edges (in `solid`'s orders) are given before healing, carried through it. +/// Operands with `counts` faces and edges. +pub(crate) fn heal_build( + solid: Solid, + size: f64, + keep: &[Vec], + faces: Option>, + edges: Option>>, + counts: &[(usize, usize)], +) -> Result { + let (healed, map) = crate::heal::heal_traced(solid, size, keep); + let carried = faces.zip(edges).zip(map).and_then(|((f, e), m)| m.carry(&f, &e)); + let (faces, edges) = carried.unzip(); + record::build(healed, counts, faces, edges.as_ref()) +} + +/// The body truck's result `s` makes, healed (see [`heal_build`]), with its history `h`. +pub(crate) fn truck_result(s: Solid, h: Option, size: f64, keep: &[Vec], counts: &[(usize, usize)]) -> Result { + let (faces, edges) = h.unzip(); + heal_build(s, size, keep, faces, edges, counts) +} + +/// `body` healed (see [`heal_build`]), its history carried through: `solid` is `body`'s solid +/// or a copy of it (`Body::deep_copy`, which keeps its order). +pub(crate) fn heal_body(body: &Body, solid: Solid, size: f64, keep: &[Vec], counts: &[(usize, usize)]) -> Result { + let (faces, edges) = body.history().map(|h| (h.faces.clone(), h.edges.iter().map(|e| e.origins.clone()).collect())).unzip(); + heal_build(solid, size, keep, faces, edges, counts) +} + +/// The faces and edges of `a`, then of the tool `b` turned inside out as a void (`inner`, a +/// copy of `b`, with `inner_edges` its edges by identity before turning): the history of +/// `a` with `b`'s inside taken out where `b` lies wholly within it. +pub(crate) fn void( + a: &Body, + inner_edges: HashMap>, + b_faces: usize, +) -> (Vec, HashMap>) { + let faces = (0..a.face_count()) + .map(|i| record::from_face(0, i, Change::Kept)) + .chain((0..b_faces).map(|i| record::from_face(1, i, Change::Kept))) + .collect(); + let mut edges = record::edge_origins(&a.solid, 0); + edges.extend(inner_edges); + (faces, edges) +} + +#[cfg(test)] +#[path = "boolean_history_tests.rs"] +mod tests; diff --git a/crates/kernel/src/coplanar.rs b/crates/kernel/src/coplanar.rs index c7de53bf..8064336c 100644 --- a/crates/kernel/src/coplanar.rs +++ b/crates/kernel/src/coplanar.rs @@ -17,7 +17,8 @@ use solvecraft_geom::Vec3; use truck_modeling::{self as mt, builder}; use crate::body::{Body, Solid, from_p3, p3, v3}; -use crate::{BoolOp, KernelError, Result, guard}; +use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv, History}; +use crate::{BoolOp, KernelError, Result, guard, record}; #[derive(Clone, Copy, Debug)] struct PlaneFace { @@ -41,7 +42,8 @@ fn planes(b: &Body) -> Vec<(usize, PlaneFace)> { } /// Push the planar faces of `b` lying in the plane (n, d) — n their outward normal — along n -/// by `delta` (negative: inward). Their neighbours must stand perpendicular to the plane. +/// by `delta` (negative: inward). Their neighbours must stand perpendicular to the plane. The +/// result's history is over `[b]` (none where the faces had to be offset instead). pub(crate) fn push_faces(b: &Body, n: Vec3, d: f64, delta: f64) -> Result { // The exact rebuild, unless it came out unsound (a curved side it couldn't re-sweep). match push_faces_exact(b, n, d, delta) @@ -64,6 +66,7 @@ pub(crate) fn push_faces(b: &Body, n: Vec3, d: f64, delta: f64) -> Result _ => false, }) .collect(); + // An offset reports no history yet. crate::offset::offset_body(b, |i, _| if planes.get(i).copied().unwrap_or(false) { delta } else { 0.0 }).map_err(|_| e) } } @@ -75,6 +78,9 @@ pub(crate) fn push_faces_exact(b: &Body, n: Vec3, d: f64, delta: f64) -> Result< let fail = |m: &str| KernelError::Failed(format!("pushing a face: {m}")); guard("push face", || { let solid = b.deep_copy(); + // The copy's edges are b's, in order; the faces come out in place, one for one. + let mut edge_origins = record::edge_origins(&solid, 0); + let mut faces_from = Vec::new(); let shift = n * delta; let mat = mt::Matrix4::from_translation(v3(shift)); let mut shells = Vec::new(); @@ -89,7 +95,9 @@ pub(crate) fn push_faces_exact(b: &Body, n: Vec3, d: f64, delta: f64) -> Result< _ => false, }; let moving: Vec = (0..faces.len()).filter(|i| faces.get(*i).is_some_and(on_plane)).collect(); + let first = faces_from.len(); if moving.is_empty() { + faces_from.extend((first..first + faces.len()).map(|i| record::from_face(0, i, Change::Kept))); shells.push(sh.clone()); continue; } @@ -161,10 +169,18 @@ pub(crate) fn push_faces_exact(b: &Body, n: Vec3, d: f64, delta: f64) -> Result< }) .collect() }; + // Each moved or stretched edge stands for the one it replaces. + for (old, new) in &emap { + if let Some(o) = edge_origins.get(old).cloned() { + edge_origins.insert(new.id(), o); + } + } use mt::{ParametricSurface3D, SearchNearestParameter}; let mut out: Vec = Vec::new(); for (i, f) in faces.iter().enumerate() { let touches = f.edge_iter().any(|e| emap.contains_key(&e.id())); + let change = if touches { Change::Modified } else { Change::Kept }; + faces_from.push(record::from_face(0, first + i, change)); if !touches { out.push(f.clone()); continue; @@ -208,7 +224,7 @@ pub(crate) fn push_faces_exact(b: &Body, n: Vec3, d: f64, delta: f64) -> Result< shells.push(out.into()); } let s = Solid::try_new(shells).map_err(|e| fail(&e.to_string()))?; - Body::new(s) + record::build(s, &record::counts(&[b]), Some(faces_from), Some(&edge_origins)) }) } @@ -257,13 +273,18 @@ pub(crate) fn boolean_apart(a: &Body, b: &Body, op: BoolOp) -> Option Result> { let (mut a2, mut b2) = (a.clone(), b.clone()); + // What the pushed bodies are made of: their histories over [a] and [b]. + let (mut ha, mut hb) = (crate::boolean_record::kept(a, 0, 1), crate::boolean_record::kept(b, 0, 1)); for (n, d) in &same { // b's face points along n too. b2 = push_faces(&b2, *n, *d, d2)?; + hb = crate::boolean_record::through(&b2, 0, hb.as_ref()); if op == BoolOp::Union { a2 = push_faces(&a2, *n, *d, d1)?; + ha = crate::boolean_record::through(&a2, 0, ha.as_ref()); } } for (n, d) in &back { @@ -285,22 +306,77 @@ pub(crate) fn boolean_apart(a: &Body, b: &Body, op: BoolOp) -> Option r = x, + Some(x) => { + h = h.and_then(|h| trimmed(&x, h, &slab, [a, b], *n, *d, tol)); + r = x; + } None => return Ok(None), } } } - Ok(Some(r)) + Ok(Some(r.with_checked_history(h, &counts))) }; Some(run()) } +/// The history over `[a, b]` of `x`, the union `r` of `a` and `b` (with history `h` over them), +/// pushed out past the plane (n, d), with the slab beyond it (`slab`, whose start cap lies in +/// the plane) cut off. The slab's cap in `x` is where the operands' faces in that plane were +/// (all of them, so only a cap that stays one face says which), and where it meets a side of +/// an operand is that side's edge along one of those faces. `None` where that is not so. +fn trimmed(x: &Body, h: History, slab: &Body, operands: [&Body; 2], n: Vec3, d: f64, tol: f64) -> Option { + let mut cut = x.history()?.clone(); + cut.substitute(0, &h).ok()?; + let cap = slab.history()?.faces.iter().position(|f| f.origins == [FaceOrigin::Cap { end: false }])?; + let cap = FaceOrigin::Face { operand: 2, face: cap }; + // The operands' faces in the plane, facing along n, and their edges with each other face. + let in_plane: Vec> = operands + .iter() + .map(|o| planes(o).into_iter().filter(|(_, p)| p.n.dot(n) > 1.0 - 1e-9 && (p.d - d).abs() < tol * 10.0).map(|(i, _)| i).collect()) + .collect(); + let bounds: Vec>> = operands.iter().map(|o| crate::history::edge_faces(&o.solid)).collect(); + if cut.faces.iter().filter(|f| f.origins.contains(&cap)).count() != 1 { + return None; + } + for f in &mut cut.faces { + if f.origins.contains(&cap) { + let mut origins: std::collections::BTreeSet = f.origins.iter().copied().filter(|o| *o != cap).collect(); + for (operand, faces) in in_plane.iter().enumerate() { + origins.extend(faces.iter().map(|face| FaceOrigin::Face { operand, face: *face })); + } + f.change = if origins.len() > 1 { Change::Merged } else { Change::Modified }; + f.origins = origins.into_iter().collect(); + } + } + for e in &mut cut.edges { + for o in &mut e.origins { + let side = match *o { + EdgeOrigin::Meet { a, b } if a == cap => b, + EdgeOrigin::Meet { a, b } if b == cap => a, + _ => continue, + }; + let FaceOrigin::Face { operand, face } = side else { return None }; + let (plane_faces, bounds) = (in_plane.get(operand)?, bounds.get(operand)?); + let mut along = (0..bounds.len()) + .filter(|k| bounds.get(*k).is_some_and(|fs| fs.len() == 2 && fs.contains(&face) && fs.iter().any(|f| plane_faces.contains(f)))); + let (Some(edge), None) = (along.next(), along.next()) else { return None }; + *o = EdgeOrigin::Edge { operand, edge }; + } + } + cut.renumber(&[Some(0), Some(1), None], 2).ok()?; + Some(cut) +} + /// Has `a` material behind every part of b's faces in the plane (m, d) (m their outward /// normal, pointing into `a`), to `depth`? Checked at points inside the faces' triangles. fn backed(a: &Body, b: &Body, m: Vec3, d: f64, depth: f64) -> bool { @@ -398,6 +474,17 @@ pub(crate) fn glue(a: &Body, b: &Body) -> Option> { edges.insert(e.id(), mt::Edge::new(&v0, &v1, e.curve())); } let mut faces: Vec = fa_list.iter().enumerate().filter(|(i, _)| i != ia).map(|(_, f)| (*f).clone()).collect(); + // Every face but the two glued is kept; an edge along the seam is an edge of both. + let kept = |operand: usize, n: usize, skip: usize| { + (0..n).filter(move |i| *i != skip).map(move |i| record::from_face(operand, i, Change::Kept)) + }; + let from: Vec = kept(0, fa_list.len(), *ia).chain(kept(1, fb_list.len(), *ib)).collect(); + let mut origins = record::edge_origins(&a.solid, 0); + for (old, o) in record::edge_origins(&b.solid, 1) { + if let Some(new) = edges.get(&old) { + origins.entry(new.id()).or_default().extend(o); + } + } for (i, f) in fb_list.iter().enumerate() { if i == *ib { continue; @@ -419,7 +506,9 @@ pub(crate) fn glue(a: &Body, b: &Body) -> Option> { faces.push(nf); } let solid = Solid::try_new(vec![faces.into()]).map_err(|e| fail(&e.to_string()))?; - Body::new(crate::heal::heal(solid, size)) + // Each edge's origins in the solid's edge order. + let origins = crate::body::edge_order(solid.face_iter()).0.iter().map(|e| origins.get(&e.id()).cloned()).collect(); + crate::boolean_record::heal_build(solid, size, &[], Some(from), origins, &record::counts(&[a, b])) })); } } diff --git a/crates/kernel/src/heal.rs b/crates/kernel/src/heal.rs index b86c5858..ab25c048 100644 --- a/crates/kernel/src/heal.rs +++ b/crates/kernel/src/heal.rs @@ -1,12 +1,13 @@ //! Clean-up after operations: coplanar neighbouring faces become one face, and two straight //! edges meeting in line at a vertex nothing else uses become one edge (as Fusion keeps them). -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use solvecraft_geom::Vec3; use truck_modeling::{self as mt, builder}; use crate::body::{Solid, from_p3}; +use crate::history::{Change, EdgeOrigin, FaceProv}; fn plane_of(f: &mt::Face) -> Option<(Vec3, f64)> { match f.oriented_surface() { @@ -31,7 +32,7 @@ pub(crate) fn absolute_face(f: &mt::Face, wires: Vec) -> Option usize { +pub(crate) fn find(p: &mut [usize], mut i: usize) -> usize { while let Some(&q) = p.get(i) { if q == i { break; @@ -77,8 +78,6 @@ fn inside(p: Vec3, edges: &[mt::Edge], n: Vec3) -> bool { c } -/// Merge each group of edge-connected coplanar faces into one face. Returns None when nothing -/// changed or the result would not be a valid solid. /// Do the points lie along one of the lines faces were split along on purpose (polylines; /// chords of a curved line sag by under a tenth of their length)? pub(crate) fn on_split_line(pts: &[Vec3], keep: &[Vec]) -> bool { @@ -98,7 +97,10 @@ pub(crate) fn edge_points(e: &mt::Edge) -> Vec { (0..=4).map(|k| from_p3(c.subs(t0 + (t1 - t0) * k as f64 / 4.0))).collect() } -fn merge_coplanar(shell: &mt::Shell, tol: f64, keep: &[Vec]) -> Option { +/// Merge each group of edge-connected coplanar faces into one face (keeping faces apart along +/// the lines in `keep`). Returns the new shell and, for each of its faces, the faces of `shell` +/// it was made from (by position); None when nothing changed or a merged face can't be built. +fn merge_coplanar(shell: &mt::Shell, tol: f64, keep: &[Vec]) -> Option<(mt::Shell, Vec>)> { let faces: Vec = shell.face_iter().cloned().collect(); let planes: Vec> = faces.iter().map(plane_of).collect(); let mut parent: Vec = (0..faces.len()).collect(); @@ -136,12 +138,14 @@ fn merge_coplanar(shell: &mt::Shell, tol: f64, keep: &[Vec]) -> Option = Vec::new(); + let mut from: Vec> = Vec::new(); let mut order: Vec = groups.keys().copied().collect(); order.sort_unstable(); for g in order { let members = groups.get(&g)?; if members.len() == 1 { out.push(faces.get(*members.first()?)?.clone()); + from.push(members.clone()); continue; } let (n, _) = (*planes.get(*members.first()?)?)?; @@ -189,13 +193,14 @@ fn merge_coplanar(shell: &mt::Shell, tol: f64, keep: &[Vec]) -> Option Option { +fn merge_collinear(shell: &mt::Shell, trace: &mut Trace) -> Option { let mut faces: Vec = shell.face_iter().cloned().collect(); let mut changed = false; for _round in 0..10_000 { @@ -222,6 +227,7 @@ fn merge_collinear(shell: &mt::Shell) -> Option { }); let Some((a, b, va, vb)) = pair else { break }; let joined = builder::line(&va, &vb); + trace.made(&joined, [a.id(), b.id()]); for f in &mut faces { if !f.edge_iter().any(|e| e.id() == a.id()) { continue; @@ -292,12 +298,13 @@ fn fit_exact(e: &mt::Edge, tol: f64) -> Option { } /// Replace approximate edges that are really lines or arcs with exact ones. -fn exact_edges(shell: &mt::Shell, tol: f64) -> Option { +fn exact_edges(shell: &mt::Shell, tol: f64, trace: &mut Trace) -> Option { let mut subst: HashMap = HashMap::new(); for e in shell.edge_iter() { if !subst.contains_key(&e.id()) && let Some(x) = exact_curve(&e, tol) { + trace.made(&x, [e.id()]); subst.insert(e.id(), x); } } @@ -398,45 +405,171 @@ fn planar_surfaces(shell: &mt::Shell, tol: f64) -> Option { } /// Heal a solid; returns the input unchanged when nothing applies or healing fails. +#[cfg(test)] pub fn heal(solid: Solid, size: f64) -> Solid { heal_keep(solid, size, &[]) } /// [`heal`], keeping faces apart along lines they were split along on purpose. pub(crate) fn heal_keep(solid: Solid, size: f64, keep: &[Vec]) -> Solid { - // Edges a boolean made are intersection curves, which panic where they no longer - // evaluate; the solid then stays as it is. - crate::guard("heal", || Ok(heal_shells(&solid, size, keep))).ok().flatten().unwrap_or(solid) + healed(solid, size, keep, false).0 } -fn heal_shells(solid: &Solid, size: f64, keep: &[Vec]) -> Option { +/// What healing made each face and edge of its result from. +#[derive(Debug)] +pub(crate) struct HealMap { + /// For each face of the result (in its face order), the input faces it was made from (by + /// position in the input's face order): one, or several merged into it. + pub faces: Vec>, + /// For each edge of the result (by identity), the input edges it was made from (by + /// position in the input's edge order): one, or several joined into it. + pub edges: HashMap>, +} + +impl HealMap { + /// The history of the healed solid, from that of its input (`faces` in the input's face + /// order, each a region of operand faces; `edges` the origins of each edge, in its edge + /// order): a face healing merged from several is Merged when they came from several, the + /// rest keep their origins (see [`crate::record::regions`]). Faces in the healed solid's + /// face order, edges by identity. + pub(crate) fn carry(&self, faces: &[FaceProv], edges: &[Vec]) -> Option<(Vec, HashMap>)> { + let regions = self + .faces + .iter() + .map(|under| { + let whole = matches!(under.as_slice(), [i] if faces.get(*i).is_some_and(|f| f.change == Change::Kept)); + let mut origins = BTreeSet::new(); + for i in under { + origins.extend(faces.get(*i)?.origins.iter().copied()); + } + Some((origins, whole)) + }) + .collect::>>()?; + let edges = self + .edges + .iter() + .map(|(id, under)| { + let mut origins = BTreeSet::new(); + for i in under { + origins.extend(edges.get(*i)?.iter().copied()); + } + Some((*id, origins.into_iter().collect())) + }) + .collect::>>()?; + Some((crate::record::regions(regions), edges)) + } + + /// Every face and edge of `solid` as itself (nothing healed). + fn identity(solid: &Solid) -> HealMap { + let faces = (0..solid.face_iter().count()).map(|i| vec![i]).collect(); + let edges = crate::record::edge_index(solid).into_iter().map(|(id, i)| (id, vec![i])).collect(); + HealMap { faces, edges } + } +} + +/// [`heal_keep`], with what each face and edge of the result was made from (`None` when that +/// could not be followed). +pub(crate) fn heal_traced(solid: Solid, size: f64, keep: &[Vec]) -> (Solid, Option) { + healed(solid, size, keep, true) +} + +/// The healed solid, and what it was made from when `traced`. +fn healed(solid: Solid, size: f64, keep: &[Vec], traced: bool) -> (Solid, Option) { + // Edges a boolean made are intersection curves, which panic where they no longer + // evaluate; the solid then stays as it is. + match crate::guard("heal", || Ok(heal_shells(&solid, size, keep, traced))) { + Ok(Some((healed, map))) => (healed, map), + _ => { + let map = if traced { crate::guard("heal map", || Ok(HealMap::identity(&solid))).ok() } else { None }; + (solid, map) + } + } +} + +/// The input edges each edge made while healing comes from. Input edges live as long as the +/// healing (the input solid is borrowed throughout), so their identities are never reused; an +/// edge healing makes is recorded when it is made, from edges then alive, which overwrites any +/// entry left under its identity by an edge made earlier and dropped since. Records nothing +/// when not `on`. +struct Trace { + on: bool, + input: HashMap, + made: HashMap>, +} + +impl Trace { + /// The input edges under the live edge `id`. + fn of(&self, id: &mt::EdgeID) -> Vec { + match self.made.get(id) { + Some(from) => from.clone(), + None => self.input.get(id).map(|i| vec![*i]).unwrap_or_default(), + } + } + + /// `edge` was made from the live edges `from`. + fn made(&mut self, edge: &mt::Edge, from: [mt::EdgeID; N]) { + if !self.on { + return; + } + let mut under: Vec = from.iter().flat_map(|id| self.of(id)).collect(); + under.sort_unstable(); + under.dedup(); + self.made.insert(edge.id(), under); + } +} + +fn heal_shells(solid: &Solid, size: f64, keep: &[Vec], traced: bool) -> Option<(Solid, Option)> { let tol = (size * 1e-7).max(1e-9); let mut shells = Vec::new(); let mut changed = false; + let input = if traced { crate::record::edge_index(solid) } else { HashMap::new() }; + let mut trace = Trace { on: traced, input, made: HashMap::new() }; + // Each face of the shells so far, as the input faces it was made from. + let mut faces: Vec> = Vec::new(); + let mut first = 0; for sh in solid.boundaries() { let mut cur = sh.clone(); + let count = cur.face_iter().count(); + let mut from: Vec> = (first..first + count).map(|i| vec![i]).collect(); + first += count; + // Faces come out in place, one for one, except where coplanar ones merge. if let Some(m) = planar_surfaces(&cur, (size * 1e-7).max(1e-9)) { cur = m; changed = true; } - if let Some(m) = exact_edges(&cur, (size * 1e-5).max(1e-7)) { + if let Some(m) = exact_edges(&cur, (size * 1e-5).max(1e-7), &mut trace) { cur = m; changed = true; } - if let Some(m) = merge_coplanar(&cur, tol, keep) { + if let Some((m, groups)) = merge_coplanar(&cur, tol, keep) { cur = m; + from = groups.iter().map(|g| g.iter().filter_map(|i| from.get(*i)).flatten().copied().collect()).collect(); changed = true; } - if let Some(m) = merge_collinear(&cur) { + if let Some(m) = merge_collinear(&cur, &mut trace) { cur = m; changed = true; } + faces.extend(from); shells.push(cur); } if !changed { return None; } - Solid::try_new(shells).ok() + let healed = Solid::try_new(shells).ok()?; + if !traced { + return Some((healed, None)); + } + let edges: Option>> = crate::body::edge_order(healed.face_iter()) + .0 + .iter() + .map(|e| { + let under = trace.of(&e.id()); + (!under.is_empty()).then(|| (e.id(), under)) + }) + .collect(); + let map = edges.map(|edges| HealMap { faces, edges }); + Some((healed, map)) } #[cfg(test)] diff --git a/crates/kernel/src/history.rs b/crates/kernel/src/history.rs index 82fedf14..af1e84dd 100644 --- a/crates/kernel/src/history.rs +++ b/crates/kernel/src/history.rs @@ -240,6 +240,75 @@ impl History { Ok(()) } + /// Renumber the operands: operand `i` becomes `map[i]` of `operands` (several may become + /// one, which joins their origins), or is dropped (`None`), which fails, without changing + /// `self`, where an origin lies on it. + pub(crate) fn renumber(&mut self, map: &[Option], operands: usize) -> Result<()> { + if map.len() != self.operands || map.iter().flatten().any(|op| *op >= operands) { + return Err(failed("operand map does not match")); + } + let to = |op: usize| map.get(op).copied().flatten().ok_or_else(|| failed("an origin lies on a dropped operand")); + let face = |o: FaceOrigin| -> Result { + Ok(match o { + FaceOrigin::Face { operand, face } => FaceOrigin::Face { operand: to(operand)?, face }, + FaceOrigin::Edge { operand, edge } => FaceOrigin::Edge { operand: to(operand)?, edge }, + FaceOrigin::Offset { operand, face } => FaceOrigin::Offset { operand: to(operand)?, face }, + other => other, + }) + }; + let mut faces = Vec::with_capacity(self.faces.len()); + for f in &self.faces { + let origins: BTreeSet = f.origins.iter().map(|o| face(*o)).collect::>()?; + // Origins that became one are one face: no longer a merge. + let change = if f.change == Change::Merged && origins.len() == 1 { Change::Modified } else { f.change }; + faces.push(FaceProv { origins: origins.into_iter().collect(), change }); + } + let mut edges = Vec::with_capacity(self.edges.len()); + for e in &self.edges { + let origins: BTreeSet = e + .origins + .iter() + .map(|o| match *o { + EdgeOrigin::Edge { operand, edge } => Ok(EdgeOrigin::Edge { operand: to(operand)?, edge }), + EdgeOrigin::Meet { a, b } => Ok(EdgeOrigin::Meet { a: face(a)?, b: face(b)? }), + }) + .collect::>()?; + edges.push(EdgeProv { faces: e.faces.clone(), origins: origins.into_iter().collect() }); + } + self.operands = operands; + self.faces = faces; + self.edges = edges; + Ok(()) + } + + /// [`History::renumber`], consuming. + pub(crate) fn renumbered(mut self, map: &[Option], operands: usize) -> Result { + self.renumber(map, operands)?; + Ok(self) + } + + /// [`History::substitute`], consuming. + pub(crate) fn substituted(mut self, k: usize, inner: &History) -> Result { + self.substitute(k, inner)?; + Ok(self) + } + + /// This history, whose operand `i` was made by the operation `parts[i]` describes (all of + /// them over the same operands), as a history over those operands. + pub(crate) fn over(mut self, parts: &[&History]) -> Result { + let n = parts.first().map_or(0, |p| p.operands); + if parts.len() != self.operands || parts.iter().any(|p| p.operands != n) { + return Err(failed("operand histories do not match")); + } + // Each part's operands come in at its place: then part i's operand j is i * n + j. + for (k, part) in parts.iter().enumerate().rev() { + self.substitute(k, part)?; + } + let map: Vec> = (0..self.operands).map(|op| op.checked_rem(n)).collect(); + self.renumber(&map, n)?; + Ok(self) + } + /// The result's face sources, carried from its operands' through this history: a face /// kept, modified or split from one face keeps that face's source; a merged face keeps the /// source its origins share, if they share one; a generated face has none. diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index ad8cbe33..803b2723 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -17,6 +17,7 @@ mod blend_corner; mod blend_variable; mod body; mod boolean_diagnostics; +mod boolean_record; mod bore_guard; mod build; mod coplanar; diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs index fc41c51a..08602b9c 100644 --- a/crates/kernel/src/ops.rs +++ b/crates/kernel/src/ops.rs @@ -6,7 +6,8 @@ use truck_modeling as mt; use crate::body::{Body, Solid, from_p3}; use crate::boolean_diagnostics::{BooleanFallback, BooleanStrategy, capture_boolean_diagnostics, merge, record}; -use crate::{KernelError, Result, guard}; +use crate::boolean_record::{self as recorded, Traced}; +use crate::{KernelError, Result, guard, record}; #[cfg(test)] #[path = "bore_union_tests.rs"] @@ -112,8 +113,15 @@ fn overlap(a: &solvecraft_geom::Mesh, b: &solvecraft_geom::Mesh) -> Option<(f64, /// Booleans that need no intersection: one body clear of the other, or inside it. These are /// common with coincident curved faces (a hole drilled where one already is, a body joined -/// with itself), which the intersection can't handle. -fn trivial(a: &Body, b: &Body, op: BoolOp, ma: &solvecraft_geom::Mesh, mb: &solvecraft_geom::Mesh) -> Option> { +/// with itself), which the intersection can't handle. `counts`: the operands' faces and edges. +fn trivial( + a: &Body, + b: &Body, + op: BoolOp, + ma: &solvecraft_geom::Mesh, + mb: &solvecraft_geom::Mesh, + counts: &[(usize, usize)], +) -> Option> { let (b_in_a, a_in_b) = overlap(ma, mb)?; const NONE: f64 = 0.003; const ALL: f64 = 0.997; @@ -122,20 +130,21 @@ fn trivial(a: &Body, b: &Body, op: BoolOp, ma: &solvecraft_geom::Mesh, mb: &solv // Sampling only selects this candidate. Validate every shell boundary and the // complete material/void nesting before preserving a disconnected union. BoolOp::Union if disjoint => { - let joined = join_pieces(vec![a.clone(), b.clone()]).ok().flatten()?; + let pieces = vec![(a.clone(), recorded::kept(a, 0, 2)), (b.clone(), recorded::kept(b, 1, 2))]; + let joined = recorded::join(pieces, counts).ok().flatten()?; joined.separated_lumps().ok()?; Some(Some(joined)) } - BoolOp::Cut if disjoint => Some(Some(a.clone())), + BoolOp::Cut if disjoint => Some(Some(recorded::operand(a, 0, counts))), BoolOp::Cut if a_in_b > ALL => Some(None), BoolOp::Intersect if disjoint => Some(None), - BoolOp::Intersect if b_in_a > ALL => Some(Some(b.clone())), - BoolOp::Intersect if a_in_b > ALL => Some(Some(a.clone())), - BoolOp::Union if b_in_a > ALL => Some(Some(a.clone())), - BoolOp::Union if a_in_b > ALL => Some(Some(b.clone())), + BoolOp::Intersect if b_in_a > ALL => Some(Some(recorded::operand(b, 1, counts))), + BoolOp::Intersect if a_in_b > ALL => Some(Some(recorded::operand(a, 0, counts))), + BoolOp::Union if b_in_a > ALL => Some(Some(recorded::operand(a, 0, counts))), + BoolOp::Union if a_in_b > ALL => Some(Some(recorded::operand(b, 1, counts))), // The tool wholly inside, clear of the outside: a void. BoolOp::Cut if b_in_a > ALL => { - let candidate = void(a, b).ok().flatten()?; + let candidate = void(a, b, counts).ok().flatten()?; candidate.separated_lumps().ok()?; Some(Some(candidate)) } @@ -155,24 +164,28 @@ mod shortcut_tests { for distance in [19.999, 20.001] { let other = crate::sphere(direction * distance, 10.0).unwrap(); let other_mesh = other.tessellate(0.03).unwrap(); - assert!(trivial(&outer, &other, BoolOp::Union, &mesh, &other_mesh).is_none()); - assert!(trivial(&other, &outer, BoolOp::Union, &other_mesh, &mesh).is_none()); + let counts = record::counts(&[&outer, &other]); + assert!(trivial(&outer, &other, BoolOp::Union, &mesh, &other_mesh, &counts).is_none()); + assert!(trivial(&other, &outer, BoolOp::Union, &other_mesh, &mesh, &counts).is_none()); } let protruding = crate::sphere(direction * 9.001, 1.0).unwrap(); let protruding_mesh = protruding.tessellate(0.03).unwrap(); - assert!(trivial(&outer, &protruding, BoolOp::Cut, &mesh, &protruding_mesh).is_none()); + let counts = record::counts(&[&outer, &protruding]); + assert!(trivial(&outer, &protruding, BoolOp::Cut, &mesh, &protruding_mesh, &counts).is_none()); } } /// `a` with the inside of `b` (wholly within it) taken out: a second, inward-facing shell. -fn void(a: &Body, b: &Body) -> Result> { +fn void(a: &Body, b: &Body, counts: &[(usize, usize)]) -> Result> { guard("void", || { let mut inner = b.deep_copy(); + let inner_edges = record::edge_origins(&inner, 1); inner.not(); let mut shells = a.solid.boundaries().clone(); shells.extend(inner.boundaries().iter().cloned()); let s = crate::body::Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("void: {e}")))?; - Ok(Some(Body::new(s)?)) + let (faces, edges) = recorded::void(a, inner_edges, b.face_count()); + Ok(Some(record::build(s, counts, Some(faces), Some(&edges))?)) }) } @@ -192,6 +205,9 @@ pub(crate) fn narrowing_bores() -> bool { /// Boolean of two bodies. The result may be empty (`Ok(None)`) for a cut that removes /// everything or an intersection of disjoint bodies. Results are checked against volume bounds /// and retried with shifted copies and other tolerances when they fail or look wrong. +/// +/// The result carries its exact history ([`Body::history`]) over the operands `[a, b]`, +/// whichever way it was made, or none where a step it took does not report one. pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { let (mut r, mut diagnostics) = capture_boolean_diagnostics(|| boolean_whole(a, b, op)); // Bodies in several pieces: when the whole failed (or came out empty), a piece at a time. @@ -203,7 +219,7 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { if retry && (a.solid.boundaries().len() > 1 || b.solid.boundaries().len() > 1) { let (la, lb) = (a.lumps()?, b.lumps()?); if la.len() > 1 || lb.len() > 1 { - let (pieces, report) = capture_boolean_diagnostics(|| by_lumps(la, lb, op)); + let (pieces, report) = capture_boolean_diagnostics(|| by_lumps(a, b, la, lb, op)); if let Ok(Some(x)) = pieces { r = Ok(Some(x)); diagnostics = report; @@ -283,6 +299,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { a.require_brep("a boolean")?; b.require_brep("a boolean")?; let size = a.size().max(b.size()); + let counts = record::counts(&[a, b]); // The checks mesh both bodies; where they meet in a region much thinner than they are (a // thin part trimmed by a long one), a step from their size would facet the small faces // there coarsely enough to fail the membership test on a correct result. The meshes are @@ -318,7 +335,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { }; // One body clear of or inside the other: no intersection needed. if let (Some(ma), Some(mb)) = &meshes - && let Some(r) = trivial(a, b, op, ma, mb) + && let Some(r) = trivial(a, b, op, ma, mb, &counts) { return Ok(r); } @@ -332,7 +349,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { // general B-rep boolean does, retaining intentional Split Face lines. let keep: Vec> = a.split_keep().iter().chain(b.split_keep()).cloned().collect(); let r = if op == BoolOp::Union { - r.map(|body| guard("heal prism boolean", || Body::new(crate::heal::heal_keep((*body.solid).clone(), size, &keep))).unwrap_or(body)) + r.map(|body| guard("heal prism boolean", || recorded::heal_body(&body, (*body.solid).clone(), size, &keep, &counts)).unwrap_or(body)) } else { r }; @@ -384,7 +401,8 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { BORES.with(|c| c.set(false)); if let Some(body) = r { merge(diagnostics); - return Ok(Some(body)); + // Narrowing a bore (an offset) reports no history yet. + return Ok(Some(body.with_checked_history(None, &counts))); } } let mut empty_votes = 0; @@ -394,25 +412,9 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { let shift = mt::Vector3::new(j[0], j[1], j[2]) * (size * 0.01); for k in [2e-3, 5e-4] { let tol = (size * k).max(1e-5); - let r = guard("boolean", || { - let (mut sa, mut sb) = (a.deep_copy(), b.deep_copy()); - if attempt > 0 { - sa = mt::builder::translated(&sa, shift); - sb = mt::builder::translated(&sb, shift); - } - let res = match op { - BoolOp::Union => truck_shapeops::or(&sa, &sb, tol), - BoolOp::Cut => { - sb.not(); - truck_shapeops::and(&sa, &sb, tol) - } - BoolOp::Intersect => truck_shapeops::and(&sa, &sb, tol), - }; - let s = res.ok_or_else(|| KernelError::Failed("no result".into()))?; - Ok(if attempt > 0 { mt::builder::translated(&s, -shift) } else { s }) - }); + let r = truck_boolean(a, b, op, if attempt > 0 { Some(shift) } else { None }, tol); match r { - Ok(s) if s.boundaries().is_empty() || s.face_iter().next().is_none() => { + Ok((s, _)) if s.boundaries().is_empty() || s.face_iter().next().is_none() => { empty_votes += 1; if attempt > 0 || k != 2e-3 { let mut event = @@ -423,7 +425,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { } last = "empty result".into(); } - Ok(s) => match guard("boolean result", || Body::new(crate::heal::heal_keep(s, size, &keep))) { + Ok((s, h)) => match guard("boolean result", || recorded::truck_result(s, h, size, &keep, &counts)) { Ok(body) => { let (ok, v) = check(&body); if ok { @@ -497,8 +499,9 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { APART.with(|c| c.set(true)); let c = b.solid.vertex_iter().fold(Vec3::ZERO, |acc, v| acc + from_p3(v.point())) * (1.0 / b.solid.vertex_iter().count().max(1) as f64); let translation = Vec3::new(0.61, -0.37, 0.71) * (size * 2e-6); - let (r, diagnostics) = - capture_boolean_diagnostics(|| transform(b, translation, c, Vec3::new(0.27, 0.83, -0.49), 2e-6).and_then(|nb| boolean(a, &nb, op))); + let (r, diagnostics) = capture_boolean_diagnostics(|| { + transform(b, translation, c, Vec3::new(0.27, 0.83, -0.49), 2e-6).and_then(|nb| Ok(boolean(a, &nb, op)?.map(|r| nudged(r, &nb, &counts)))) + }); APART.with(|c| c.set(false)); match r { Ok(Some(body)) => { @@ -527,8 +530,42 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result> { Err(KernelError::Failed(format!("boolean {op:?}: {last}"))) } -/// A boolean of bodies given as their pieces; the result's pieces in one body. -fn by_lumps(la: Vec, lb: Vec, op: BoolOp) -> Result> { +/// `r`, a boolean of the target and `nb` (the tool nudged), with its history over the target and +/// the tool itself (the nudge keeps every face and edge). +pub(crate) fn nudged(r: Body, nb: &Body, counts: &[(usize, usize)]) -> Body { + let h = recorded::through(&r, 1, nb.history()); + r.with_checked_history(h, counts) +} + +/// What truck's boolean makes of `a` and `b`: the solid, and its history in our terms (see +/// [`recorded::shapeops`]). On copies of the operands moved by `shift` (when given; the result +/// is moved back), which changes the rays the boolean classifies faces by. Copies (and moved +/// copies) keep the operands' face and edge order, and so does moving the result back, so the +/// history truck records holds for the operands themselves. +pub(crate) fn truck_boolean(a: &Body, b: &Body, op: BoolOp, shift: Option, tol: f64) -> Result<(Solid, Option)> { + guard("boolean", || { + let (mut sa, mut sb) = (a.deep_copy(), b.deep_copy()); + if let Some(shift) = shift { + sa = mt::builder::translated(&sa, shift); + sb = mt::builder::translated(&sb, shift); + } + let tool_edges = (op == BoolOp::Cut).then(|| record::edge_origins(&sb, 1)); + let res = match op { + BoolOp::Union => truck_shapeops::or_with_history(&sa, &sb, tol), + BoolOp::Cut => { + sb.not(); + truck_shapeops::and_with_history(&sa, &sb, tol) + } + BoolOp::Intersect => truck_shapeops::and_with_history(&sa, &sb, tol), + }; + let (s, h) = res.ok_or_else(|| KernelError::Failed("no result".into()))?; + let h = h.and_then(|h| recorded::shapeops(&h, &sb, tool_edges.as_ref())); + Ok((if let Some(shift) = shift { mt::builder::translated(&s, -shift) } else { s }, h)) + }) +} + +/// A boolean of `a` and `b` given as their pieces (`la`, `lb`); the result's pieces in one body. +pub(crate) fn by_lumps(a: &Body, b: &Body, la: Vec, lb: Vec, op: BoolOp) -> Result> { let boxes_meet = |x: &Body, y: &Body| -> bool { let (Ok(mx), Ok(my)) = (x.tessellate(x.size() * 0.05 + 1e-3), y.tessellate(y.size() * 0.05 + 1e-3)) else { return true }; let (bx, by) = (mx.bounds(), my.bounds()); @@ -540,14 +577,26 @@ fn by_lumps(la: Vec, lb: Vec, op: BoolOp) -> Result> { && bx.min.z <= by.max.z + g && by.min.z <= bx.max.z + g }; - let mut out: Vec = Vec::new(); + // Each piece with its history over [a, b] (a piece's faces and edges are its body's own). + let traced = |pieces: Vec, whole: &Body, operand: usize| -> Vec { + let histories = recorded::parts_of(&pieces, whole); + pieces.into_iter().zip(histories).map(|(p, h)| (p, recorded::placed(h, operand, 2))).collect() + }; + let (la, lb) = (traced(la, a, 0), traced(lb, b, 1)); + let pair = |(x, hx): &Traced, (y, hy): &Traced, op: BoolOp| -> Result> { + Ok(boolean(x, y, op)?.map(|r| { + let h = recorded::of_pair(&r, hx, hy); + (r, h) + })) + }; + let mut out: Vec = Vec::new(); match op { BoolOp::Cut => { for x in la { let mut cur = Some(x); for y in &lb { cur = match cur { - Some(c) if boxes_meet(&c, y) => boolean(&c, y, BoolOp::Cut)?, + Some(c) if boxes_meet(&c.0, &y.0) => pair(&c, y, BoolOp::Cut)?, other => other, }; } @@ -557,8 +606,8 @@ fn by_lumps(la: Vec, lb: Vec, op: BoolOp) -> Result> { BoolOp::Intersect => { for x in &la { for y in &lb { - if boxes_meet(x, y) - && let Some(r) = boolean(x, y, BoolOp::Intersect)? + if boxes_meet(&x.0, &y.0) + && let Some(r) = pair(x, y, BoolOp::Intersect)? { out.push(r); } @@ -571,9 +620,9 @@ fn by_lumps(la: Vec, lb: Vec, op: BoolOp) -> Result> { let mut cur = y; let mut rest = Vec::new(); for x in out { - if boxes_meet(&x, &cur) { - match boolean(&x, &cur, BoolOp::Union)? { - Some(u) if u.solid.boundaries().len() == 1 => cur = u, + if boxes_meet(&x.0, &cur.0) { + match pair(&x, &cur, BoolOp::Union)? { + Some(u) if u.0.solid.boundaries().len() == 1 => cur = u, _ => rest.push(x), } } else { @@ -585,7 +634,7 @@ fn by_lumps(la: Vec, lb: Vec, op: BoolOp) -> Result> { } } } - join_pieces(out) + recorded::join(out, &record::counts(&[a, b])) } /// Bodies whose bounding boxes are clearly apart, as one body of several pieces, without any @@ -779,8 +828,14 @@ pub fn split_by_plane(body: &Body, plane: &solvecraft_geom::Plane) -> Result Result> { + boolean(body, &half_box(body, plane, positive)?, BoolOp::Intersect) +} + +/// A box covering everything of `body` on one side of a plane, its start cap in the plane. +pub(crate) fn half_box(body: &Body, plane: &solvecraft_geom::Plane, positive: bool) -> Result { body.require_brep("split")?; let size = body.size(); let mut bb = solvecraft_geom::Aabb3::EMPTY; @@ -799,8 +854,7 @@ pub(crate) fn half_space(body: &Body, plane: &solvecraft_geom::Plane, positive: holes: vec![], }; let (lo, hi) = if positive { (0.0, reach) } else { (-reach, 0.0) }; - let half = crate::build::extrude(plane, std::slice::from_ref(&sq), lo, hi)?.pop().ok_or_else(|| KernelError::Failed("half space".into()))?; - boolean(body, &half, BoolOp::Intersect) + crate::build::extrude(plane, std::slice::from_ref(&sq), lo, hi)?.pop().ok_or_else(|| KernelError::Failed("half space".into())) } #[cfg(test)] diff --git a/crates/kernel/src/polybool.rs b/crates/kernel/src/polybool.rs index 3d0055ae..980438ba 100644 --- a/crates/kernel/src/polybool.rs +++ b/crates/kernel/src/polybool.rs @@ -3,13 +3,16 @@ //! as a wall flush with the side of its base. The polygons are rebuilt into a B-rep: coplanar //! pieces are merged into faces, T-junctions are split so neighbouring faces share edges. -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use solvecraft_geom::{Mesh, Vec3}; use truck_modeling::{self as mt, builder}; -use crate::body::{Body, Solid, p3}; -use crate::{BoolOp, KernelError, Result, guard}; +use crate::body::{Body, Solid, edge_order, from_p3, p3}; +use crate::boolean_record::{self as recorded, Traced}; +use crate::heal::find; +use crate::history::{EdgeOrigin, FaceOrigin, History}; +use crate::{BoolOp, KernelError, Result, guard, record}; const MAX_DEPTH: usize = 4000; const MAX_POLYS: usize = 200_000; @@ -30,6 +33,8 @@ impl PlaneEq { struct Poly { v: Vec, plane: PlaneEq, + /// The operand face this is (a piece of). + src: FaceOrigin, } impl Poly { @@ -95,10 +100,10 @@ fn split(pl: PlaneEq, p: &Poly, eps: f64, cf: &mut Vec, cb: &mut Vec } } if fv.len() >= 3 { - f.push(Poly { v: fv, plane: p.plane }); + f.push(Poly { v: fv, plane: p.plane, src: p.src }); } if bv.len() >= 3 { - b.push(Poly { v: bv, plane: p.plane }); + b.push(Poly { v: bv, plane: p.plane, src: p.src }); } } } @@ -263,8 +268,8 @@ fn face_planes(b: &Body, faces: &[crate::FaceInfo]) -> std::result::Result std::result::Result, NotPlanar> { +/// Polygons of a body whose faces are all planar, as pieces of its faces as operand `operand`. +fn planar_polys(b: &Body, operand: usize) -> std::result::Result, NotPlanar> { let tol = (b.size() * 1e-3).max(1e-3); let unmeshed = |e: KernelError| NotPlanar::Other(format!("faces could not be meshed ({e})")); let faces = b.faces(tol).map_err(unmeshed)?; @@ -284,13 +289,14 @@ fn planar_polys(b: &Body) -> std::result::Result, NotPlanar> { if (v[1] - v[0]).cross(v[2] - v[0]).dot(plane.n) < 0.0 { v.reverse(); } - out.push(Poly { v, plane }); + out.push(Poly { v, plane, src: FaceOrigin::Face { operand, face: *fi as usize } }); } Ok(out) } -/// Polygons of any body: planar faces exactly, curved ones as their triangles (within `tol`). -fn faceted_polys(b: &Body, tol: f64) -> Option> { +/// Polygons of any body: planar faces exactly, curved ones as their triangles (within `tol`); +/// pieces of its faces as operand `operand`. +fn faceted_polys(b: &Body, tol: f64, operand: usize) -> Option> { let faces = b.faces(tol).ok()?; let mesh = b.tessellate(tol).ok()?; let planes: Vec> = faces.iter().map(|f| f.plane_normal.map(|n| PlaneEq { n, w: n.dot(f.centroid) })).collect(); @@ -309,7 +315,7 @@ fn faceted_polys(b: &Body, tol: f64) -> Option> { if (v[1] - v[0]).cross(v[2] - v[0]).dot(plane.n) < 0.0 { v.reverse(); } - out.push(Poly { v, plane }); + out.push(Poly { v, plane, src: FaceOrigin::Face { operand, face: *fi as usize } }); } (out.len() <= MAX_POLYS).then_some(out) } @@ -317,20 +323,27 @@ fn faceted_polys(b: &Body, tol: f64) -> Option> { /// Boolean of a body, its curved faces faceted to `tol`, with a closed triangle mesh (wound /// outward); the result has planar faces only. For tools the B-rep boolean cannot take, such as /// the helical groove of a thread. +/// +/// The result's history is over `[a, b]`, the mesh's faces being its triangles (and it having +/// no edges): a face of the result lies on faces of `a` (a facet of a curved one) or on +/// triangles of `b`; an edge between facets of one face of `a`, or between triangles of `b`, +/// has no origin. pub fn faceted_boolean(a: &Body, b: &Mesh, op: BoolOp, tol: f64) -> Result> { a.require_brep("a boolean")?; if !(tol.is_finite() && tol > 0.0) { return Err(KernelError::Invalid("facet tolerance".into())); } - let Some(pa) = faceted_polys(a, tol) else { + let Some(pa) = faceted_polys(a, tol, 0) else { return Err(KernelError::Failed("the body is too detailed to facet".into())); }; let mut pb = Vec::with_capacity(b.triangles.len()); - for t in &b.triangles { + for (face, t) in b.triangles.iter().enumerate() { let Some([p, q, r]) = b.tri(t) else { continue }; let Some(n) = (q - p).cross(r - p).normalized() else { continue }; - pb.push(Poly { v: vec![p, q, r], plane: PlaneEq { n, w: n.dot(p) } }); + pb.push(Poly { v: vec![p, q, r], plane: PlaneEq { n, w: n.dot(p) }, src: FaceOrigin::Face { operand: 1, face } }); } + let edges = OperandEdges::of(a); + let sources = Sources { counts: vec![(a.face_count(), edges.ends.len()), (b.triangles.len(), 0)], edges: vec![Some(edges), None] }; if pa.len() + pb.len() > MAX_POLYS { return Err(KernelError::Failed("the bodies are too detailed to facet".into())); } @@ -362,7 +375,7 @@ pub fn faceted_boolean(a: &Body, b: &Mesh, op: BoolOp, tol: f64) -> Result return Ok(Some(b)), Err(e) => last = e, } @@ -374,7 +387,7 @@ pub fn faceted_boolean(a: &Body, b: &Mesh, op: BoolOp, tol: f64) -> Result Result> { a.require_brep("a boolean")?; b.require_brep("a boolean")?; - let (pa, pb) = match (planar_polys(a), planar_polys(b)) { + let (pa, pb) = match (planar_polys(a, 0), planar_polys(b, 1)) { (Ok(pa), Ok(pb)) => (pa, pb), (ra, rb) => { if let Some(r) = sliced_planar_boolean(a, b, op) { @@ -389,7 +402,7 @@ pub fn planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { } }; let size = a.size().max(b.size()); - polys_boolean(pa, pb, size, op, size * 1e-9) + polys_boolean(pa, pb, size, op, size * 1e-9, &Sources::of(&[a, b])) } /// [`planar_boolean`] of a body with curved faces and an all-planar one, when a slab between @@ -397,7 +410,7 @@ pub fn planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Result> { /// hole): `a` is split across the slab, the middle piece takes the exact boolean, and the /// pieces outside are joined back on. `None` when no such slab exists. fn sliced_planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Option>> { - let pb = planar_polys(b).ok()?; + let pb = planar_polys(b, 1).ok()?; let size = a.size().max(b.size()); let tol = (a.size() * 1e-3).max(1e-3); let infos = a.faces(tol).ok()?; @@ -460,47 +473,140 @@ fn sliced_planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Option Result> { let fail = |m: &str| KernelError::Failed(format!("planar boolean beside curved faces: {m}")); - let mut middle = a.clone(); - let mut outside = Vec::new(); - for (cut, keep_positive) in [(lo, true), (hi, false)] { + // Every piece with its history over [a, b, then the boxes of each cut: the one away + // from the middle, the one around it]; the boxes' faces are gone from the result (glued + // away), so they are dropped at the end. + let n = 6; + let mut middle: Traced = (a.clone(), recorded::kept(a, 0, n)); + let mut outside: Vec = Vec::new(); + for (slot, (cut, keep_positive)) in [(2, (lo, true)), (4, (hi, false))] { let Some(x) = cut else { continue }; let plane = solvecraft_geom::Plane::new(axis * x, u, w).ok_or_else(|| fail("plane"))?; - outside.extend(crate::ops::half_space(&middle, &plane, !keep_positive)?); - middle = crate::ops::half_space(&middle, &plane, keep_positive)?.ok_or_else(|| fail("nothing beside the tool"))?; + let (away, kept) = (crate::ops::half_box(&middle.0, &plane, !keep_positive)?, crate::ops::half_box(&middle.0, &plane, keep_positive)?); + let (h_away, h_kept) = (recorded::kept(&away, slot, n), recorded::kept(&kept, slot + 1, n)); + if let Some(o) = crate::ops::boolean(&middle.0, &away, BoolOp::Intersect)? { + let h = recorded::of_pair(&o, &middle.1, &h_away); + outside.push((o, h)); + } + let m = crate::ops::boolean(&middle.0, &kept, BoolOp::Intersect)?.ok_or_else(|| fail("nothing beside the tool"))?; + let h = recorded::of_pair(&m, &middle.1, &h_kept); + middle = (m, h); } - let pm = planar_polys(&middle).map_err(|e| fail(&e.describe("middle piece")))?; - let r = polys_boolean(pm, pb, size, op, size * 1e-9)?; + let pm = planar_polys(&middle.0, 0).map_err(|e| fail(&e.describe("middle piece")))?; + let r = polys_boolean(pm, pb, size, op, size * 1e-9, &Sources::of(&[&middle.0, b]))?; + let h_b = recorded::kept(b, 1, n); + let counts = record::counts(&[a, b]); if op == BoolOp::Intersect { - return Ok(r); + return Ok(r.map(|r| { + let h = dropped(recorded::of_pair(&r, &middle.1, &h_b)); + r.with_checked_history(h, &counts) + })); } // The rebuild leaves the cut faces' edges split where other polygons were cut; healed, // they match the pieces outside edge for edge. - let mut pieces: Vec = match r { - Some(m) => vec![Body::new(crate::heal::heal_keep(m.deep_copy(), size, a.split_keep()))?], + let mut pieces: Vec = match r { + Some(m) => { + let healed = recorded::heal_body(&m, m.deep_copy(), size, a.split_keep(), &record::counts(&[&middle.0, b]))?; + let h = recorded::of_pair(&healed, &middle.1, &h_b); + vec![(healed, h)] + } None => Vec::new(), }; // Each piece outside meets the middle along its cut face (glued one shell to another). - if pieces.iter().chain(&outside).any(|x| x.solid.boundaries().len() != 1) { + if pieces.iter().chain(&outside).any(|(x, _)| x.solid.boundaries().len() != 1) { return Err(fail("a piece in several parts")); } - for o in outside { - let joined = pieces.iter().enumerate().find_map(|(i, m)| Some((i, crate::coplanar::glue(m, &o)?.ok()?))); + for (o, h_o) in outside { + let joined = pieces.iter().enumerate().find_map(|(i, (m, _))| Some((i, crate::coplanar::glue(m, &o)?.ok()?))); let (i, j) = joined.ok_or_else(|| fail("a piece does not join back"))?; if let Some(slot) = pieces.get_mut(i) { - *slot = j; + let h = recorded::of_pair(&j, &slot.1, &h_o); + *slot = (j, h); } } - crate::ops::join_pieces(pieces) + recorded::join(pieces.into_iter().map(|(p, h)| (p, dropped(h))).collect(), &counts) }; Some(run()) } -fn polys_boolean(pa: Vec, pb: Vec, size: f64, op: BoolOp, eps: f64) -> Result> { +/// `h` over `[a, b]`: the operands after them (the boxes a piece was cut with) dropped, which +/// fails where something of them is left. +fn dropped(h: Option) -> Option { + let h = h?; + let map: Vec> = (0..h.operands).map(|op| (op < 2).then_some(op)).collect(); + recorded::composed(h.renumbered(&map, 2)) +} + +fn polys_boolean(pa: Vec, pb: Vec, size: f64, op: BoolOp, eps: f64, sources: &Sources) -> Result> { let polys = csg(pa, pb, op, eps)?; if polys.is_empty() { return Ok(None); } - rebuild(polys, size).map(Some) + rebuild(polys, size, sources).map(Some) +} + +/// What a rebuilt body's history is over: each operand's face and edge counts and, where it is +/// a B-rep, its edges in its edge order (the faces each bounds, and its ends). +struct Sources { + counts: Vec<(usize, usize)>, + edges: Vec>, +} + +/// An operand's edges in its edge order (each one's ends), and those between each pair of its +/// faces (lesser face first). +struct OperandEdges { + ends: Vec<(Vec3, Vec3)>, + between: HashMap<(usize, usize), Vec>, +} + +impl OperandEdges { + fn of(b: &Body) -> OperandEdges { + let (edges, bounds) = edge_order(b.solid.face_iter()); + let mut between: HashMap<(usize, usize), Vec> = HashMap::new(); + for (i, fs) in bounds.iter().enumerate() { + if let [f, g] = fs.as_slice() { + between.entry((*f.min(g), *f.max(g))).or_default().push(i); + } + } + let ends = edges.iter().map(|e| (from_p3(e.absolute_front().point()), from_p3(e.absolute_back().point()))).collect(); + OperandEdges { ends, between } + } +} + +impl Sources { + fn of(bodies: &[&Body]) -> Sources { + let edges: Vec = bodies.iter().map(|b| OperandEdges::of(b)).collect(); + let counts = bodies.iter().zip(&edges).map(|(b, e)| (b.face_count(), e.ends.len())).collect(); + Sources { counts, edges: edges.into_iter().map(Some).collect() } + } + + /// The origins of an edge between a piece of `p` and a piece of `q` running from `ends.0` + /// to `ends.1`: where faces of two operands meet; the edge between two faces of one + /// operand (the one the piece lies along, when they share several); none between pieces of + /// one face, or between triangles of a mesh operand. `None` when it can't be told. + fn edge(&self, p: FaceOrigin, q: FaceOrigin, ends: (Vec3, Vec3), tol: f64) -> Option> { + let (FaceOrigin::Face { operand: x, face: f }, FaceOrigin::Face { operand: y, face: g }) = (p, q) else { return None }; + if x != y { + return Some(vec![EdgeOrigin::Meet { a: p.min(q), b: p.max(q) }]); + } + let Some(edges) = self.edges.get(x)?.as_ref() else { return Some(Vec::new()) }; + if f == g { + return Some(Vec::new()); + } + let between = edges.between.get(&(f.min(g), f.max(g)))?; + let along: Vec = match between.as_slice() { + [_] => between.clone(), + _ => between + .iter() + .copied() + .filter(|i| edges.ends.get(*i).is_some_and(|(s, t)| ends.0.dist_to_segment(*s, *t) <= tol && ends.1.dist_to_segment(*s, *t) <= tol)) + .collect(), + }; + match along.as_slice() { + [edge] => Some(vec![EdgeOrigin::Edge { operand: x, edge: *edge }]), + _ => None, + } + } } /// Enclosed volume of a closed set of polygons (divergence theorem). @@ -566,7 +672,10 @@ fn key(p: Vec3, q: f64) -> (i64, i64, i64) { } /// Turn polygons into a B-rep solid: merge by plane, split T-junctions, chain boundary loops. -fn rebuild(polys: Vec, size: f64) -> Result { +/// Its history over the operands the polygons are pieces of (`sources`): each face the +/// operand faces whose pieces it joins (pieces joined across the edges between them), each edge +/// what the pieces either side of it say (see [`Sources::edge`]). +fn rebuild(polys: Vec, size: f64, sources: &Sources) -> Result { let q = size * 1e-7; // Global vertex table. let mut index: HashMap<(i64, i64, i64), usize> = HashMap::new(); @@ -590,6 +699,7 @@ fn rebuild(polys: Vec, size: f64) -> Result { verts.len() - 1 }; let mut loops: Vec<(PlaneEq, Vec)> = Vec::new(); + let mut srcs: Vec = Vec::new(); for p in &polys { let mut l: Vec = p.v.iter().map(|v| id(*v, &mut verts)).collect(); l.dedup(); @@ -598,6 +708,7 @@ fn rebuild(polys: Vec, size: f64) -> Result { } if l.len() >= 3 { loops.push((p.plane, l)); + srcs.push(p.src); } } // Split every polygon edge at vertices lying on it (T-junctions). Vertices are binned in a @@ -616,9 +727,11 @@ fn rebuild(polys: Vec, size: f64) -> Result { grid.entry(ckey(*v)).or_default().push(k); } let mut directed: Vec<(usize, usize, usize)> = Vec::new(); // (plane group, from, to) + // The polygons (by loop) each directed edge comes from. + let mut owners: HashMap<(usize, usize, usize), Vec> = HashMap::new(); let mut groups: Vec = Vec::new(); let mut group_of: HashMap<(i64, i64, i64, i64), usize> = HashMap::new(); - for (pl, l) in &loops { + for (li, (pl, l)) in loops.iter().enumerate() { let gk = ((pl.n.x * 1e8).round() as i64, (pl.n.y * 1e8).round() as i64, (pl.n.z * 1e8).round() as i64, (pl.w / (tol * 10.0)).round() as i64); let g = *group_of.entry(gk).or_insert_with(|| { groups.push(*pl); @@ -660,9 +773,11 @@ fn rebuild(polys: Vec, size: f64) -> Result { let mut prev = a; for (_, k) in on { directed.push((g, prev, k)); + owners.entry((g, prev, k)).or_default().push(li); prev = k; } directed.push((g, prev, b)); + owners.entry((g, prev, b)).or_default().push(li); } } // Interior edges cancel against their reverse within the same plane group. @@ -678,10 +793,41 @@ fn rebuild(polys: Vec, size: f64) -> Result { } } boundary.sort_unstable(); + // Pieces either side of an edge inside a plane group lie in one face. + let mut parent: Vec = (0..loops.len()).collect(); + for ((g, a, b), here) in &owners { + let (Some(there), Some(&first)) = (owners.get(&(*g, *b, *a)), here.first()) else { continue }; + for other in here.iter().chain(there) { + let (rp, rq) = (find(&mut parent, first), find(&mut parent, *other)); + if let Some(slot) = parent.get_mut(rp) { + *slot = rq; + } + } + } + let roots: Vec = (0..loops.len()).map(|li| find(&mut parent, li)).collect(); + let mut joined: HashMap> = HashMap::new(); + for (root, src) in roots.iter().zip(&srcs) { + joined.entry(*root).or_default().insert(*src); + } + // What lies on each side of each boundary edge (from, to): the faces of its polygons. + let mut side: HashMap<(usize, usize), BTreeSet> = HashMap::new(); + for (g, a, b) in &boundary { + let set = side.entry((*a, *b)).or_default(); + for li in owners.get(&(*g, *a, *b)).into_iter().flatten() { + set.extend(srcs.get(*li)); + } + } + // The faces of one boundary loop's polygons. + let loop_faces = |g: usize, lp: &[usize]| -> Option> { + let (a, b) = (*lp.first()?, *lp.get(1)?); + let li = *owners.get(&(g, a, b))?.first()?; + joined.get(roots.get(li)?).cloned() + }; guard("planar boolean rebuild", || { let tv: Vec = verts.iter().map(|v| builder::vertex(p3(*v))).collect(); let mut edges: HashMap<(usize, usize), mt::Edge> = HashMap::new(); let mut faces: Vec = Vec::new(); + let mut face_from: HashMap>> = HashMap::new(); for (g, plane) in groups.iter().enumerate() { let mut segs: Vec<(usize, usize)> = boundary.iter().filter(|e| e.0 == g).map(|e| (e.1, e.2)).collect(); let mut wires: Vec = Vec::new(); @@ -785,7 +931,7 @@ fn rebuild(polys: Vec, size: f64) -> Result { ws.push(wires.get(*h).cloned().ok_or_else(|| KernelError::Failed("wire".into()))?); } } - let _ = ol; + let from = loop_faces(g, ol); // A loop a hair off flat (fine facets of a curved face) takes the group's plane. let face = match builder::try_attach_plane(&ws) { Ok(f) => f, @@ -796,13 +942,29 @@ fn rebuild(polys: Vec, size: f64) -> Result { .map_err(|e2| KernelError::Failed(format!("planar boolean face: {e}; {e2}")))? } }; + face_from.insert(face.id(), from); faces.push(face); } } let shell: mt::Shell = faces.into(); let parts = shell.connected_components(); let solid = Solid::try_new(parts).map_err(|e| KernelError::Failed(format!("planar boolean: {e}")))?; - Body::new(solid) + let regions = solid.face_iter().map(|f| face_from.get(&f.id()).cloned().flatten().map(|o| (o, false))).collect::>>(); + let tol = size * 1e-6; + let edge_from = edges + .iter() + .map(|((a, b), e)| { + let ends = (*verts.get(*a)?, *verts.get(*b)?); + let mut origins = BTreeSet::new(); + for p in side.get(&(*a, *b))? { + for q in side.get(&(*b, *a))? { + origins.extend(sources.edge(*p, *q, ends, tol)?); + } + } + Some((e.id(), origins.into_iter().collect())) + }) + .collect::>>>(); + record::build(solid, &sources.counts, regions.map(record::regions), edge_from.as_ref()) }) } diff --git a/crates/kernel/src/prism.rs b/crates/kernel/src/prism.rs index 9a33493e..75264efb 100644 --- a/crates/kernel/src/prism.rs +++ b/crates/kernel/src/prism.rs @@ -12,6 +12,9 @@ use truck_modeling as mt; use crate::body::{Body, from_p3}; use crate::{BoolOp, KernelError, Result}; +#[path = "prism_lineage.rs"] +mod lineage; + const MAX_PLANAR_SEGMENTS: usize = 8192; const MAX_PLANAR_EVENTS: usize = 131_072; const MAX_PLANAR_PAIRS: usize = 16_000_000; @@ -113,11 +116,222 @@ pub fn extrude_planar_regions(plane: &Plane, regions: &[Region2], start: f64, en }) } -/// A prism: the plane of its bottom cap (normal along the walls), its height and section. +/// A prism: the plane of its bottom cap (normal along the walls), its height and section (its +/// segments' tags alongside). struct Prism { lo: f64, hi: f64, regions: Vec, + tags: Vec>>, +} + +impl Prism { + fn new(lo: f64, hi: f64, section: Section) -> Prism { + Prism { lo, hi, regions: section.regions, tags: section.tags } + } + + fn section(&self) -> Section { + Section { regions: self.regions.clone(), tags: self.tags.clone() } + } +} + +/// What a section's segment was made from, for the history of what is built over it: an +/// operand's cap edge (`edge`, by its place in the operand's edge order), the wall face beside it +/// (`wall`), and the operand's vertices (by [`Index`]) the segment starts and ends at (`None` +/// where it was split off a longer one). `None` fields: not known (no history then). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Tag { + pub operand: usize, + pub wall: Option, + pub edge: Option, + pub start: Option, + pub end: Option, +} + +/// A segment nothing is known of. +const BLANK: Tag = Tag { operand: 0, wall: None, edge: None, start: None, end: None }; + +impl Tag { + /// The segment run the other way. + fn reversed(self) -> Tag { + Tag { start: self.end, end: self.start, ..self } + } + + /// Piece `k` of `n` the segment was split into, in its direction. + fn piece(self, k: usize, n: usize) -> Tag { + Tag { start: if k == 0 { self.start } else { None }, end: if k + 1 == n { self.end } else { None }, ..self } + } +} + +/// An operand's [`Index`], built when first needed (only bodies that are prisms are read). +pub(crate) struct LazyIndex<'a> { + body: &'a Body, + operand: usize, + index: std::cell::OnceCell, +} + +impl<'a> LazyIndex<'a> { + fn new(body: &'a Body, operand: usize) -> LazyIndex<'a> { + LazyIndex { body, operand, index: std::cell::OnceCell::new() } + } + + fn get(&self) -> &Index { + self.index.get_or_init(|| Index::new(self.body, self.operand)) + } +} + +/// Regions with a tag for each segment: per region, per loop (outer, then holes), per segment. +#[derive(Clone, Debug, Default, PartialEq)] +pub(crate) struct Section { + pub regions: Vec, + pub tags: Vec>>, +} + +impl Section { + /// Regions whose segments nothing is known of. + fn untagged(regions: &[Region2]) -> Section { + let tags = regions.iter().map(|r| std::iter::once(&r.outer).chain(&r.holes).map(|l| vec![BLANK; l.segs.len()]).collect()).collect(); + Section { regions: regions.to_vec(), tags } + } + + /// The tag of segment `s` of loop `l` of region `r` (blank where missing). + fn tag(&self, r: usize, l: usize, s: usize) -> Tag { + self.tags.get(r).and_then(|t| t.get(l)).and_then(|t| t.get(s)).copied().unwrap_or(BLANK) + } + + /// The tags of region `r`'s loops, as long as its loops (blank where missing). + fn region_tags(&self, r: usize) -> Vec> { + self.regions + .get(r) + .map(|reg| { + std::iter::once(®.outer) + .chain(®.holes) + .enumerate() + .map(|(l, lp)| (0..lp.segs.len()).map(|s| self.tag(r, l, s)).collect()) + .collect() + }) + .unwrap_or_default() + } + + /// One region from its loops (each segment tagged): the loop with the largest area is the + /// outer one. + fn from_loops(loops: Vec<(Loop2, Vec)>) -> Option
{ + let ((outer, outer_tags), holes) = split_outer(loops)?; + let (holes, hole_tags): (Vec, Vec>) = holes.into_iter().unzip(); + Some(Section { regions: vec![Region2 { outer, holes }], tags: vec![std::iter::once(outer_tags).chain(hole_tags).collect()] }) + } + + /// Region `r` alone. + fn region(&self, r: usize) -> Section { + Section { regions: self.regions.get(r).cloned().into_iter().collect(), tags: vec![self.region_tags(r)] } + } + + /// Every segment in order (regions, then loops) with its tag. + fn segments(&self) -> (Vec, Vec) { + let mut out = (Vec::new(), Vec::new()); + for (r, reg) in self.regions.iter().enumerate() { + for (l, lp) in std::iter::once(®.outer).chain(®.holes).enumerate() { + for (s, seg) in lp.segs.iter().enumerate() { + out.0.push(*seg); + out.1.push(self.tag(r, l, s)); + } + } + } + out + } + + /// Loops oriented: outers counter-clockwise, holes clockwise; the tags following their + /// segments. + fn oriented(&self) -> Section { + let turn = |lp: &Loop2, tags: Vec, reverse: bool| -> (Loop2, Vec) { + if reverse { (lp.reversed(), tags.into_iter().rev().map(Tag::reversed).collect()) } else { (lp.clone(), tags) } + }; + let mut out = Section::default(); + for (r, reg) in self.regions.iter().enumerate() { + let mut tags = self.region_tags(r).into_iter(); + let (outer, outer_tags) = turn(®.outer, tags.next().unwrap_or_default(), reg.outer.signed_area() < 0.0); + let mut loop_tags = vec![outer_tags]; + let mut holes = Vec::new(); + for h in ®.holes { + let (hole, hole_tags) = turn(h, tags.next().unwrap_or_default(), h.signed_area() > 0.0); + holes.push(hole); + loop_tags.push(hole_tags); + } + out.regions.push(Region2 { outer, holes }); + out.tags.push(loop_tags); + } + out + } +} + +/// An operand's faces, edges and vertices by identity, to tag the section segments read off its +/// caps ([`Tag`]). +pub(crate) struct Index { + operand: usize, + /// Each edge's place in the edge order. + edges: std::collections::HashMap, + /// Per edge (edge order): the faces it bounds, and its ends (by vertex number). + bounds: Vec<(Vec, Option, Option)>, + /// The edges between each pair of faces (lesser first; a face twice for a seam). + between: std::collections::HashMap<(usize, usize), Vec>, + /// Each vertex's number (first seen along the edge order). + vertices: std::collections::HashMap, +} + +impl Index { + fn new(b: &Body, operand: usize) -> Index { + let (edges, bounds) = crate::body::edge_order(b.solid.face_iter()); + let mut vertices = std::collections::HashMap::new(); + for e in &edges { + for v in [e.absolute_front(), e.absolute_back()] { + let next = vertices.len(); + vertices.entry(v.id()).or_insert(next); + } + } + let mut between: std::collections::HashMap<(usize, usize), Vec> = std::collections::HashMap::new(); + for (i, fs) in bounds.iter().enumerate() { + match fs.as_slice() { + [f] => between.entry((*f, *f)).or_default().push(i), + [f, g] => between.entry((*f.min(g), *f.max(g))).or_default().push(i), + _ => {} + } + } + let bounds = edges + .iter() + .zip(bounds) + .map(|(e, fs)| (fs, vertices.get(&e.absolute_front().id()).copied(), vertices.get(&e.absolute_back().id()).copied())) + .collect(); + Index { operand, edges: crate::record::edge_index(&b.solid), bounds, between, vertices } + } + + /// The tag of the segments read off edge `e` (as it runs in a wire) of cap face `cap`. + fn tag(&self, e: &mt::Edge, cap: usize) -> Tag { + let edge = self.edges.get(&e.id()).copied(); + let wall = edge.and_then(|i| self.bounds.get(i)).and_then(|(fs, _, _)| match fs.as_slice() { + [x, y] if *x == cap => Some(*y), + [x, y] if *y == cap => Some(*x), + _ => None, + }); + Tag { operand: self.operand, wall, edge, start: self.vertices.get(&e.front().id()).copied(), end: self.vertices.get(&e.back().id()).copied() } + } + + /// The loops of cap face `f` (number `cap`) as segments, each with its tag; `None` where an + /// edge isn't a line or an arc in the plane. + fn loops(&self, f: &mt::Face, cap: usize, plane: &Plane, tol: f64, work: &mut usize, segments: &mut usize) -> Option)>> { + let mut loops = Vec::new(); + for w in f.boundaries() { + let (mut segs, mut tags) = (Vec::new(), Vec::new()); + for e in w.edge_iter() { + let pieces = crate::prism_curve::segment(e, plane, tol, work)?; + *segments = segments.checked_sub(pieces.len())?; + let tag = self.tag(e, cap); + tags.extend((0..pieces.len()).map(|k| tag.piece(k, pieces.len()))); + segs.extend(pieces); + } + loops.push((Loop2 { segs }, tags)); + } + Some(loops) + } } /// Certify that a curved wall is swept along `d` from its surface representation. @@ -209,19 +423,25 @@ fn wall_along(surface: &mt::Surface, d: Vec3, tol: f64, remaining: &mut usize) - } /// The body as a prism along `d` (unit), seen in `plane` (normal `d`). +#[cfg(test)] fn prism_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option { + prism_tagged(b, d, plane, tol, &LazyIndex::new(b, 0)) +} + +/// [`prism_of`], its section's segments tagged from `index` (the body's). +fn prism_tagged(b: &Body, d: Vec3, plane: &Plane, tol: f64, index: &LazyIndex) -> Option { // A simple extrusion with N section edges has N walls plus its cap faces. // Even disconnected two-arc sections need at most 2N faces. Keep that // complete planar arrangement envelope, independently of stepped limits. let mut remaining_faces = MAX_PLANAR_SEGMENTS * 2; let mut lo = f64::INFINITY; let mut hi = f64::NEG_INFINITY; - let mut tops: Vec<&mt::Face> = Vec::new(); + let mut tops: Vec<(usize, &mt::Face)> = Vec::new(); let mut top_h = Vec::new(); let mut bottom_h = Vec::new(); let mut wall_work = 262_144; let mut curve_work = 2_000_000usize; - for f in b.solid.face_iter() { + for (fi, f) in b.solid.face_iter().enumerate() { remaining_faces = remaining_faces.checked_sub(1)?; curve_work = curve_work.checked_sub(1)?; let surf = f.oriented_surface(); @@ -230,7 +450,7 @@ fn prism_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option { let n = Vec3::new(n.x, n.y, n.z).normalized()?; let h = plane.height(from_p3(p.origin())); if n.dot(d) > 1.0 - 1e-9 { - tops.push(f); + tops.push((fi, f)); top_h.push(h); hi = hi.max(h); continue; @@ -253,30 +473,22 @@ fn prism_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option { if top_h.iter().any(|h| (h - hi).abs() > tol) || bottom_h.iter().any(|h| (h - lo).abs() > tol) || !(lo.is_finite() && hi > lo + tol) { return None; } - let mut regions = Vec::new(); + let mut section = Section::default(); let mut remaining_segments = MAX_PLANAR_SEGMENTS; let mut remaining_loops = MAX_PLANAR_SEGMENTS; - for f in tops { - let mut loops = Vec::new(); - for w in f.boundaries() { - remaining_loops = remaining_loops.checked_sub(1)?; - let mut segs = Vec::new(); - for e in w.edge_iter() { - let segments = crate::prism_curve::segment(e, plane, tol, &mut curve_work)?; - remaining_segments = remaining_segments.checked_sub(segments.len())?; - segs.extend(segments); - } - loops.push(Loop2 { segs }); - } - let (outer, holes) = split_outer(loops)?; - regions.push(Region2 { outer, holes }); + for (fi, f) in tops { + remaining_loops = remaining_loops.checked_sub(f.boundaries().len())?; + let loops = index.get().loops(f, fi, plane, tol, &mut curve_work, &mut remaining_segments)?; + let one = Section::from_loops(loops)?; + section.regions.extend(one.regions); + section.tags.extend(one.tags); } - Some(Prism { lo, hi, regions }) + Some(Prism::new(lo, hi, section)) } /// The loop with the largest area is the outer one. -fn split_outer(mut loops: Vec) -> Option<(Loop2, Vec)> { - let i = (0..loops.len()).max_by(|a, b| loops[*a].signed_area().abs().total_cmp(&loops[*b].signed_area().abs()))?; +fn split_outer(mut loops: Vec<(Loop2, T)>) -> Option<((Loop2, T), Vec<(Loop2, T)>)> { + let i = (0..loops.len()).max_by(|a, b| loops[*a].0.signed_area().abs().total_cmp(&loops[*b].0.signed_area().abs()))?; let outer = loops.remove(i); Some((outer, loops)) } @@ -435,29 +647,23 @@ fn dir_at(regions: &[Region2], p: Vec2, tol: f64) -> Option { None } -/// Loops oriented: outers counter-clockwise, holes clockwise. -fn oriented(regions: &[Region2]) -> Vec { - regions - .iter() - .map(|r| Region2 { - outer: if r.outer.signed_area() < 0.0 { r.outer.reversed() } else { r.outer.clone() }, - holes: r.holes.iter().map(|h| if h.signed_area() > 0.0 { h.reversed() } else { h.clone() }).collect(), - }) - .collect() -} - /// 2D boolean of regions bounded by lines and arcs. fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option> { + boolean2d_tagged(&Section::untagged(a), &Section::untagged(b), op, tol).map(|s| s.regions) +} + +/// [`boolean2d`] of tagged sections: each segment of the result tagged as the segment of `a` or +/// `b` it is (a piece of). +fn boolean2d_tagged(a: &Section, b: &Section, op: BoolOp, tol: f64) -> Option
{ let count = |rs: &[Region2]| rs.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).fold(0usize, |n, lp| n.saturating_add(lp.segs.len())); - let (na, nb) = (count(a), count(b)); + let (na, nb) = (count(&a.regions), count(&b.regions)); if na.saturating_add(nb) > MAX_PLANAR_SEGMENTS || na.saturating_mul(nb) > MAX_PLANAR_PAIRS { return None; } - let (a, b) = (oriented(a), oriented(b)); - let segs = - |rs: &[Region2]| -> Vec { rs.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).flat_map(|l| l.segs.clone()).collect() }; - let (sa, sb) = (segs(&a), segs(&b)); + let (a, b) = (a.oriented(), b.oriented()); + let ((sa, ta), (sb, tb)) = (a.segments(), b.segments()); + let (a, b) = (a.regions, b.regions); if sa.iter().chain(&sb).any(|s| !matches!(s, Seg2::Line { .. } | Seg2::Arc { .. })) { return None; } @@ -526,13 +732,15 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option = Vec::new(); + let mut kept: Vec<(Seg2, Tag)> = Vec::new(); // A's pieces. for (i, s) in sa.iter().enumerate() { if cut_a.get(i)?.len() > MAX_PLANAR_SEGMENTS { return None; } - for p in pieces(s, cut_a.get_mut(i)?) { + let split = pieces(s, cut_a.get_mut(i)?); + let (n, tag) = (split.len(), ta.get(i).copied().unwrap_or(BLANK)); + for (k, p) in split.into_iter().enumerate() { let m = p.point_at(0.5); let on = dist_to(&b, m) < tol; let keep = if on { @@ -552,7 +760,7 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option= MAX_PLANAR_SEGMENTS { return None; } - kept.push(p); + kept.push((p, tag.piece(k, n))); } } } @@ -561,7 +769,9 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option MAX_PLANAR_SEGMENTS { return None; } - for p in pieces(t, cut_b.get_mut(j)?) { + let split = pieces(t, cut_b.get_mut(j)?); + let (n, tag) = (split.len(), tb.get(j).copied().unwrap_or(BLANK)); + for (k, p) in split.into_iter().enumerate() { let m = p.point_at(0.5); if dist_to(&a, m) < tol { continue; @@ -570,26 +780,28 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option= MAX_PLANAR_SEGMENTS { return None; } + let tag = tag.piece(k, n); match op { - BoolOp::Union if !ins => kept.push(p), - BoolOp::Intersect if ins => kept.push(p), - BoolOp::Cut if ins => kept.push(p.reversed()), + BoolOp::Union if !ins => kept.push((p, tag)), + BoolOp::Intersect if ins => kept.push((p, tag)), + BoolOp::Cut if ins => kept.push((p.reversed(), tag.reversed())), _ => {} } } } // Chain into loops. - let mut loops: Vec = Vec::new(); + let mut loops: Vec<(Loop2, Vec)> = Vec::new(); let mut used = vec![false; kept.len()]; for start in 0..kept.len() { if used.get(start).copied().unwrap_or(true) { continue; } - let mut segs = vec![*kept.get(start)?]; + let (seg, tag) = *kept.get(start)?; + let (mut segs, mut tags) = (vec![seg], vec![tag]); if let Some(u) = used.get_mut(start) { *u = true; } - let first = kept.get(start)?.start(); + let first = seg.start(); let mut guard = 0; loop { guard += 1; @@ -604,8 +816,9 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option tol * 10.0 { + .min_by(|x, y| kept[*x].0.start().dist(end).total_cmp(&kept[*y].0.start().dist(end)))?; + let (seg, tag) = *kept.get(next)?; + if seg.start().dist(end) > tol * 10.0 { if end.dist(first) < tol * 10.0 { break; } @@ -614,23 +827,29 @@ fn boolean2d(a: &[Region2], b: &[Region2], op: BoolOp, tol: f64) -> Option tol * tol { - loops.push(l); + loops.push((l, tags)); } } // Outers (counter-clockwise) take the holes they contain, the smallest first. - let (mut outers, holes): (Vec, Vec) = loops.into_iter().partition(|l| l.signed_area() > 0.0); - outers.sort_by(|x, y| x.signed_area().abs().total_cmp(&y.signed_area().abs())); - let mut regions: Vec = outers.into_iter().map(|o| Region2 { outer: o, holes: Vec::new() }).collect(); - for h in holes { - let p = h.segs.first()?.point_at(0.5); - let r = regions.iter_mut().find(|r| inside_loop(&r.outer, p) && r.outer.signed_area().abs() > h.signed_area().abs())?; - r.holes.push(h); + let (mut outers, holes): (Vec<(Loop2, Vec)>, Vec<(Loop2, Vec)>) = loops.into_iter().partition(|(l, _)| l.signed_area() > 0.0); + outers.sort_by(|x, y| x.0.signed_area().abs().total_cmp(&y.0.signed_area().abs())); + let mut out = Section::default(); + for (o, tags) in outers { + out.regions.push(Region2 { outer: o, holes: Vec::new() }); + out.tags.push(vec![tags]); } - Some(regions) + for (h, tags) in holes { + let p = h.segs.first()?.point_at(0.5); + let r = out.regions.iter().position(|r| inside_loop(&r.outer, p) && r.outer.signed_area().abs() > h.signed_area().abs())?; + out.regions.get_mut(r)?.holes.push(h); + out.tags.get_mut(r)?.push(tags); + } + Some(out) } /// The boolean done on prism sections, or `None` when it doesn't apply. @@ -667,27 +886,35 @@ fn split_seg(seg: &Seg2, pts: &[Vec2], tol: f64) -> Option> { /// a face down over the one above less the one below. Every outline is split where any other /// meets it, so the faces share their edges. `None` when a slab is empty or a curve isn't a /// line or an arc. -fn sew_stack(plane: &Plane, d: Vec3, pa: &Prism, pb: &Prism, tol: f64) -> Option> { +fn sew_stack(plane: &Plane, d: Vec3, pa: &Prism, pb: &Prism, tol: f64, history: &lineage::Context) -> Option> { let mut levels = vec![pa.lo, pa.hi, pb.lo, pb.hi]; levels.sort_by(f64::total_cmp); levels.dedup_by(|x, y| (*x - *y).abs() < tol); let covers = |p: &Prism, z0: f64, z1: f64| p.lo <= z0 + tol && p.hi >= z1 - tol; - let mut slabs: Vec> = Vec::new(); + let mut slabs: Vec
= Vec::new(); for w in levels.windows(2) { let (z0, z1) = (*w.first()?, *w.get(1)?); let s = match (covers(pa, z0, z1), covers(pb, z0, z1)) { - (true, true) => boolean2d(&pa.regions, &pb.regions, BoolOp::Union, tol)?, - (true, false) => pa.regions.clone(), - (false, true) => pb.regions.clone(), + (true, true) => boolean2d_tagged(&pa.section(), &pb.section(), BoolOp::Union, tol)?, + (true, false) => pa.section(), + (false, true) => pb.section(), (false, false) => return None, }; - slabs.push(oriented(&s)); + slabs.push(s.oriented()); } - sew_sections(plane, d, &levels, &slabs, tol, false) + sew_sections(plane, d, &levels, &slabs, tol, false, history) } /// Sew the exact sections between successive levels, including empty gaps. -fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec], tol: f64, coalesce: bool) -> Option> { +fn sew_sections( + plane: &Plane, + d: Vec3, + levels: &[f64], + slabs: &[Section], + tol: f64, + coalesce: bool, + history: &lineage::Context, +) -> Option> { if levels.len() != slabs.len() + 1 || slabs.len() > 128 || plane.normal().dot(d) < 1. - 1e-12 @@ -699,7 +926,7 @@ fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec], let mut blocks = Vec::new(); let mut start = None; for (i, slab) in slabs.iter().enumerate() { - if slab.is_empty() { + if slab.regions.is_empty() { if let Some(first) = start.take() { blocks.push((first, i)); } @@ -718,8 +945,9 @@ fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec], let mut remaining_constructed_faces = MAX_PLANAR_SEGMENTS * 2; let mut remaining_faces = MAX_PLANAR_SEGMENTS * 2; for (first, end) in blocks { - let section = slabs.get(first).ok_or_else(|| KernelError::Failed("missing prism section".into()))?; - if slabs[first..end].iter().all(|s| s == section) { + let tagged = slabs.get(first).ok_or_else(|| KernelError::Failed("missing prism section".into()))?; + let section = &tagged.regions; + if slabs[first..end].iter().all(|s| s.regions == *section) { let edge_count = section.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).flat_map(|l| &l.segs).fold(0usize, |n, s| { n.saturating_add(match s { Seg2::Arc { sweep, .. } if sweep.abs() > 1.5 * std::f64::consts::PI => 2, @@ -731,23 +959,23 @@ fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec], .ok_or_else(|| KernelError::Failed("prism construction face budget".into()))?; // This is a NEW exact extrusion of the current Boolean section, // not copied lineage from either operand's old footprint. - for piece in crate::extrude(plane, section, levels[first], levels[end])? { + for (r, piece) in crate::extrude(plane, section, levels[first], levels[end])?.into_iter().enumerate() { remaining_faces = remaining_faces.checked_sub(piece.face_count()).ok_or_else(|| KernelError::Failed("prism output face budget".into()))?; - pieces.push(piece); + pieces.push((piece, lineage::Built::Extruded { section: tagged.region(r), lo: levels[first], hi: levels[end] })); } } else { - let piece = sew_connected_sections(plane, d, &levels[first..=end], &slabs[first..end], tol, coalesce) + let (piece, specs, faces) = sew_connected_sections(plane, d, &levels[first..=end], &slabs[first..end], tol, coalesce) .ok_or_else(|| KernelError::Failed("stepped prism construction failed".into()))??; remaining_faces = remaining_faces.checked_sub(piece.face_count()).ok_or_else(|| KernelError::Failed("prism output face budget".into()))?; - pieces.push(piece); + pieces.push((piece, lineage::Built::Sewn { specs, faces })); } } let mut old = std::collections::HashMap::new(); let mut shells = Vec::new(); let mut remaining_map_faces = MAX_PLANAR_SEGMENTS * 2; - for piece in &pieces { + for (piece, _) in &pieces { for (index, face) in piece.solid.face_iter().enumerate() { remaining_map_faces = remaining_map_faces.checked_sub(1).ok_or_else(|| KernelError::Failed("prism output face budget".into()))?; let source = piece.face_sources().and_then(|s| s.get(index)).and_then(Option::as_ref); @@ -766,7 +994,8 @@ fn sew_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec], _ => None, }) .collect(); - Ok(body.with_face_sources(Some(std::sync::Arc::new(sources)))) + let body = body.with_face_sources(Some(std::sync::Arc::new(sources))); + Ok(history.attach(body, &pieces)) })()) } @@ -781,20 +1010,29 @@ fn wall_key(s: &Seg2) -> Option<[u64; 6]> { } /// A genuinely stepped connected axial block has no single extrusion footprint. -fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec], tol: f64, coalesce: bool) -> Option> { +#[allow(clippy::type_complexity)] +fn sew_connected_sections( + plane: &Plane, + d: Vec3, + levels: &[f64], + slabs: &[Section], + tol: f64, + coalesce: bool, +) -> Option, Option>)>> { use crate::sew::{EdgeSpec, FaceSpec, SurfSpec}; // Faces at each level: up where the slab below sticks out, down where the one above does. - let diff = |x: &[Region2], y: &[Region2]| -> Option> { - match (x.is_empty(), y.is_empty()) { - (true, _) => Some(Vec::new()), - (false, true) => Some(x.to_vec()), - (false, false) => boolean2d(x, y, BoolOp::Cut, tol).map(|r| oriented(&r)), + let diff = |x: &Section, y: &Section| -> Option
{ + match (x.regions.is_empty(), y.regions.is_empty()) { + (true, _) => Some(Section::default()), + (false, true) => Some(x.clone()), + (false, false) => boolean2d_tagged(x, y, BoolOp::Cut, tol).map(|r| r.oriented()), } }; - let mut caps: Vec<(f64, bool, Vec)> = Vec::new(); + let empty = Section::default(); + let mut caps: Vec<(f64, bool, Section)> = Vec::new(); for (k, z) in levels.iter().enumerate() { - let below: &[Region2] = if k == 0 { &[] } else { slabs.get(k - 1)? }; - let above: &[Region2] = slabs.get(k).map(Vec::as_slice).unwrap_or(&[]); + let below: &Section = if k == 0 { &empty } else { slabs.get(k - 1)? }; + let above: &Section = slabs.get(k).unwrap_or(&empty); caps.push((*z, true, diff(below, above)?)); caps.push((*z, false, diff(above, below)?)); } @@ -802,7 +1040,7 @@ fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec Vec { rs.iter().flat_map(|r| std::iter::once(r.outer.clone()).chain(r.holes.iter().cloned())).collect() }; let mut pts: Vec = Vec::new(); - for lp in slabs.iter().flat_map(|s| loops_of(s)).chain(caps.iter().flat_map(|c| loops_of(&c.2))) { + for lp in slabs.iter().flat_map(|s| loops_of(&s.regions)).chain(caps.iter().flat_map(|c| loops_of(&c.2.regions))) { for sg in &lp.segs { pts.push(sg.start()); pts.push(sg.end()); @@ -820,28 +1058,43 @@ fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec = Vec::new(); let mut last = std::collections::BTreeMap::new(); let mut remaining = MAX_PLANAR_SEGMENTS * 2; + // What each face is over, for the result's history. Coalesced walls span several slabs + // and are not described: such a piece carries no history. + let mut specs: Vec = Vec::new(); for (i, slab) in slabs.iter().enumerate() { - for lp in loops_of(slab) { - for sg0 in &lp.segs { - for sg in split_seg(sg0, &pts, tol)? { - remaining = remaining.checked_sub(1)?; - let key = wall_key(&sg)?; - if coalesce { - if let Some(&index) = last.get(&key) { - let run: &mut (usize, usize, Seg2) = runs.get_mut(index)?; - if run.1 == i { - run.1 = i + 1; - continue; - } - // Duplicate oriented boundaries in one slab are not - // a valid reason to reconstruct a larger wall. - if run.1 == i + 1 { - return None; + let z0 = *levels.get(i)?; + for (r, reg) in slab.regions.iter().enumerate() { + for (l, lp) in std::iter::once(®.outer).chain(®.holes).enumerate() { + let first = specs.len(); + for (si, sg0) in lp.segs.iter().enumerate() { + let pieces = split_seg(sg0, &pts, tol)?; + let (n, tag) = (pieces.len(), slab.tag(r, l, si)); + for (k, sg) in pieces.into_iter().enumerate() { + remaining = remaining.checked_sub(1)?; + let key = wall_key(&sg)?; + if coalesce { + if let Some(&index) = last.get(&key) { + let run: &mut (usize, usize, Seg2) = runs.get_mut(index)?; + if run.1 == i { + run.1 = i + 1; + continue; + } + // Duplicate oriented boundaries in one slab are not + // a valid reason to reconstruct a larger wall. + if run.1 == i + 1 { + return None; + } } + last.insert(key, runs.len()); + } else { + specs.push(lineage::Spec::Wall { tag: tag.piece(k, n), z0, next: specs.len() + 1, at: sg.end() }); } - last.insert(key, runs.len()); + runs.push((i, i + 1, sg)); } - runs.push((i, i + 1, sg)); + } + // The loop's last wall runs into its first. + if !coalesce && let Some(lineage::Spec::Wall { next, .. }) = specs.last_mut() { + *next = first; } } } @@ -902,7 +1155,7 @@ fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec = Vec::new(); @@ -914,16 +1167,24 @@ fn sew_connected_sections(plane: &Plane, d: Vec3, levels: &[f64], slabs: &[Vec Option>> { + simple_prism_indexed(a, b, op, [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)]) +} + +/// [`prism_boolean`] of single prisms, the operands' indexes given. +fn simple_prism_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2]) -> Option>> { let size = a.size().max(b.size()); let tol = (size * 1e-7).max(1e-9) * 10.0; // Candidate directions: a's planar face normals (each line once). @@ -945,11 +1206,11 @@ fn simple_prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option= pa.hi - tol; let same = (pb.lo - pa.lo).abs() < tol && (pb.hi - pa.hi).abs() < tol; @@ -964,41 +1225,55 @@ fn simple_prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option tol * (1.0 + u) || (u - y).abs() > tol * (1.0 + u) { - return sew_stack(&plane, d, &pa, &pb, tol).map(|r| r.map(Some)); + return sew_stack(&plane, d, &pa, &pb, tol, &history).map(|r| r.map(Some)); } (pa.lo.min(pb.lo), pa.hi.max(pb.hi)) } else { (pa.lo, pa.hi) }; let base = plane.offset(lo); - let mut bodies = match crate::extrude(&base, ®ions, 0.0, hi - lo) { + let bodies = match crate::extrude(&base, regions, 0.0, hi - lo) { Ok(b) => b, Err(e) => return Some(Err(e)), }; - let first = bodies.pop()?; - if !bodies.is_empty() { - // Several pieces: one body of several lumps. - let mut shells = first.solid.boundaries().clone(); - for b in bodies { - shells.extend(b.solid.boundaries().iter().cloned()); - } - return Some(crate::body::Solid::try_new(shells).map_err(|e| crate::KernelError::Failed(e.to_string())).and_then(Body::new).map(Some)); + // Each body over its region. + let mut pieces: Vec<(Body, lineage::Built)> = + bodies.into_iter().enumerate().map(|(r, b)| (b, lineage::Built::Extruded { section: section.region(r), lo, hi })).collect(); + let last = pieces.pop()?; + if !pieces.is_empty() { + // Several pieces: one body of several lumps (the last first). + pieces.push(last); + pieces.rotate_right(1); + let shells: Vec = pieces.iter().flat_map(|(b, _)| b.solid.boundaries().iter().cloned()).collect(); + return Some( + crate::body::Solid::try_new(shells) + .map_err(|e| crate::KernelError::Failed(e.to_string())) + .and_then(Body::new) + .map(|body| Some(history.attach(body, &pieces))), + ); } - Some(Ok(Some(first))) + Some(Ok(Some(history.attach(last.0.clone(), &[last])))) } /// A stepped extrusion (case floor plus walls), recovered from its cap events. /// Downward caps add a section above their level; upward caps remove one. +#[cfg(test)] fn stack_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option> { - if let Some(p) = prism_of(b, d, plane, tol) { + stack_tagged(b, d, plane, tol, &LazyIndex::new(b, 0)) +} + +/// [`stack_of`], its sections' segments tagged from `index` (the body's). +fn stack_tagged(b: &Body, d: Vec3, plane: &Plane, tol: f64, index: &LazyIndex) -> Option> { + if let Some(p) = prism_tagged(b, d, plane, tol, index) { return Some(vec![p]); } if b.face_count() > 2048 { @@ -1007,23 +1282,15 @@ fn stack_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option> { let mut events = Vec::new(); let mut wall_work = 262_144; let mut curve_work = 2_000_000; - for f in b.solid.face_iter() { + let mut segments = usize::MAX; + for (fi, f) in b.solid.face_iter().enumerate() { let surf = f.oriented_surface(); if let mt::Surface::Plane(p) = &surf { let n = p.normal(); let n = Vec3::new(n.x, n.y, n.z).normalized()?; if n.dot(d).abs() > 1.0 - 1e-9 { - let mut loops = Vec::new(); - for w in f.boundaries() { - let mut segs = Vec::new(); - for e in w.edge_iter() { - segs.extend(crate::prism_curve::segment(e, plane, tol, &mut curve_work)?); - } - loops.push(Loop2 { segs }); - } - let (outer, holes) = split_outer(loops)?; - let region = Region2 { outer, holes }; - events.push((plane.height(from_p3(p.origin())), n.dot(d) > 0.0, oriented(&[region]))); + let section = Section::from_loops(index.get().loops(f, fi, plane, tol, &mut curve_work, &mut segments)?)?; + events.push((plane.height(from_p3(p.origin())), n.dot(d) > 0.0, section.oriented())); continue; } if n.dot(d).abs() > 1e-9 { @@ -1041,29 +1308,35 @@ fn stack_of(b: &Body, d: Vec3, plane: &Plane, tol: f64) -> Option> { if levels.len() < 2 || levels.len() > 64 || levels.iter().any(|z| !z.is_finite()) { return None; } - let mut current = Vec::new(); + let mut current = Section::default(); let mut stack = Vec::new(); for (i, z) in levels.iter().enumerate() { // Remove terminating material before adding any material starting here. for up in [true, false] { for (_, _, section) in events.iter().filter(|(h, u, _)| (*h - *z).abs() < tol && *u == up) { - current = match (current.is_empty(), up) { + current = match (current.regions.is_empty(), up) { (true, false) => section.clone(), (true, true) => return None, - (false, _) => boolean2d(¤t, section, if up { BoolOp::Cut } else { BoolOp::Union }, tol)?, + (false, _) => boolean2d_tagged(¤t, section, if up { BoolOp::Cut } else { BoolOp::Union }, tol)?, }; } } if let Some(next) = levels.get(i + 1) - && !current.is_empty() + && !current.regions.is_empty() { - stack.push(Prism { lo: *z, hi: *next, regions: oriented(¤t) }); + stack.push(Prism::new(*z, *next, current.oriented())); } } - (current.is_empty() && !stack.is_empty()).then_some(stack) + (current.regions.is_empty() && !stack.is_empty()).then_some(stack) } +#[cfg(test)] fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option>> { + stack_indexed(a, b, op, [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)]) +} + +/// [`prism_boolean`] of stepped prisms, the operands' indexes given. +fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2]) -> Option>> { // Common-cap joins and overlapping partial-depth cuts of aligned stepped prisms can // retain their exact section boundaries instead of intersecting meshes. // Nonaligned bodies retain their existing B-rep path. @@ -1073,8 +1346,8 @@ fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option>> if op == BoolOp::Cut && a.face_count().saturating_add(b.face_count()) > 2048 { return None; } - fn section(ps: &[Prism], lo: f64, hi: f64, tol: f64) -> &[Region2] { - ps.iter().find(|p| p.lo <= lo + tol && p.hi >= hi - tol).map(|p| p.regions.as_slice()).unwrap_or(&[]) + fn section(ps: &[Prism], lo: f64, hi: f64, tol: f64) -> Section { + ps.iter().find(|p| p.lo <= lo + tol && p.hi >= hi - tol).map(Prism::section).unwrap_or_default() } let tol = (a.size().max(b.size()) * 1e-7).max(1e-9) * 10.0; let mut dirs: Vec = Vec::new(); @@ -1091,8 +1364,8 @@ fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option>> let ends = |ps: &[Prism]| ps.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p.lo), hi.max(p.hi))); let (d, plane, pa, pb) = dirs.into_iter().take(8).find_map(|d| { let plane = Plane::from_normal(Vec3::ZERO, d)?; - let pa = stack_of(a, d, &plane, tol)?; - let pb = stack_of(b, d, &plane, tol)?; + let pa = stack_tagged(a, d, &plane, tol, ia)?; + let pb = stack_tagged(b, d, &plane, tol, ib)?; let (ea, eb) = (ends(&pa), ends(&pb)); match op { BoolOp::Union if (ea.0 - eb.0).abs() >= tol && (ea.1 - eb.1).abs() >= tol => return None, @@ -1114,48 +1387,46 @@ fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option>> let (lo, hi) = (*w.first()?, *w.get(1)?); let (x, y) = (section(&pa, lo, hi, tol), section(&pb, lo, hi, tol)); if op == BoolOp::Cut { - let count = edges(x).saturating_add(edges(y)); + let count = edges(&x.regions).saturating_add(edges(&y.regions)); work = work.saturating_add(count.saturating_mul(count)); if work > 32_000_000 { return None; } } - let s = match (x.is_empty(), y.is_empty(), op) { - (true, _, BoolOp::Cut | BoolOp::Intersect) | (_, true, BoolOp::Intersect) => Vec::new(), - (true, _, BoolOp::Union) => y.to_vec(), - (_, true, _) => x.to_vec(), - _ => boolean2d(x, y, op, tol)?, + let s = match (x.regions.is_empty(), y.regions.is_empty(), op) { + (true, _, BoolOp::Cut | BoolOp::Intersect) | (_, true, BoolOp::Intersect) => Section::default(), + (true, _, BoolOp::Union) => y, + (_, true, _) => x, + _ => boolean2d_tagged(&x, &y, op, tol)?, }; if op == BoolOp::Cut { - output_edges = output_edges.saturating_add(edges(&s)); + output_edges = output_edges.saturating_add(edges(&s.regions)); if output_edges > 8192 { return None; } } - slabs.push(oriented(&s)); + slabs.push(s.oriented()); } - if slabs.iter().all(Vec::is_empty) { + if slabs.iter().all(|s| s.regions.is_empty()) { return Some(Ok(None)); } - sew_sections( - &plane, - d, - &levels, - &slabs, - tol, - op == BoolOp::Cut - && !a.regularization_protected - && !b.regularization_protected - && a.split_keep().is_empty() - && b.split_keep().is_empty() - && a.paint().is_none_or(|p| p.faces.is_empty()) - && b.paint().is_none_or(|p| p.faces.is_empty()), - ) - .map(|r| r.map(Some)) + let history = lineage::Context { op, operands: [a, b], index: [ia.get(), ib.get()], plane, tol, heights: None }; + let coalesce = op == BoolOp::Cut + && !a.regularization_protected + && !b.regularization_protected + && a.split_keep().is_empty() + && b.split_keep().is_empty() + && a.paint().is_none_or(|p| p.faces.is_empty()) + && b.paint().is_none_or(|p| p.faces.is_empty()); + sew_sections(&plane, d, &levels, &slabs, tol, coalesce, &history).map(|r| r.map(Some)) } +/// Boolean of prisms along a common direction, in 2D (see the module notes); `None` when it +/// doesn't apply. The result carries its history over `[a, b]` (see [`lineage`]). pub(crate) fn prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option>> { - crate::guard("prism boolean", || Ok(simple_prism_boolean(a, b, op).or_else(|| stack_boolean(a, b, op)))).ok().flatten() + // Each operand's index is built once, and only if it is read as a prism. + let index = [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)]; + crate::guard("prism boolean", || Ok(simple_prism_indexed(a, b, op, index).or_else(|| stack_indexed(a, b, op, index)))).ok().flatten() } #[cfg(test)] @@ -1323,9 +1594,20 @@ mod planar_tests { } else { rectangle(Vec2::new(-5., -5.), Vec2::new(5., 5.)).outer.reversed() }); - let slabs = vec![vec![outer], vec![upper]]; - let old = sew_sections(&Plane::XY, Vec3::Z, &[0., 2., 20.], &slabs, 1e-7, false).unwrap().unwrap(); - let merged = sew_sections(&Plane::XY, Vec3::Z, &[0., 2., 20.], &slabs, 1e-7, true).unwrap().unwrap(); + let slabs = vec![Section::untagged(&[outer]), Section::untagged(&[upper])]; + // Sections nothing is known of: no history, the construction alone is under test. + let operand = crate::extrude(&Plane::XY, &[rectangle(Vec2::new(-1., -1.), Vec2::new(1., 1.))], 0., 1.).unwrap().pop().unwrap(); + let index = LazyIndex::new(&operand, 0); + let history = lineage::Context { + op: BoolOp::Cut, + operands: [&operand, &operand], + index: [index.get(), index.get()], + plane: Plane::XY, + tol: 1e-7, + heights: None, + }; + let old = sew_sections(&Plane::XY, Vec3::Z, &[0., 2., 20.], &slabs, 1e-7, false, &history).unwrap().unwrap(); + let merged = sew_sections(&Plane::XY, Vec3::Z, &[0., 2., 20.], &slabs, 1e-7, true, &history).unwrap().unwrap(); let count = if curved { 2 } else { 4 }; assert_eq!(old.face_count() - merged.face_count(), count); let marked: Vec<_> = merged.solid.face_iter().enumerate().filter(|(i, _)| merged.face_was_coalesced(*i)).collect(); @@ -1344,7 +1626,7 @@ mod planar_tests { assert!(mesh.contains(Vec3::new(0., 0., 1.))); assert!(!mesh.contains(Vec3::new(0., 0., 10.))); assert!(merged.validity().is_empty()); - assert_ne!(wall_key(&slabs[0][0].outer.segs[0]), wall_key(&slabs[0][0].outer.segs[0].reversed())); + assert_ne!(wall_key(&slabs[0].regions[0].outer.segs[0]), wall_key(&slabs[0].regions[0].outer.segs[0].reversed())); } } diff --git a/crates/kernel/src/prism_lineage.rs b/crates/kernel/src/prism_lineage.rs new file mode 100644 index 00000000..251ef2b3 --- /dev/null +++ b/crates/kernel/src/prism_lineage.rs @@ -0,0 +1,380 @@ +//! The history of a prism boolean's result (see [`super::prism_boolean`]), from what its faces +//! were built over: each wall over a section segment whose [`Tag`] says which operand wall it +//! was read beside, each cap over a section region lying on operand caps at its level. Edges +//! follow from the same tags: a wall's edge with a cap is the operand's edge between them (or +//! the meeting of the wall with the other operand's cap); an edge between walls at a section +//! vertex is the operand's edge there (or the meeting of two operands' walls). + +use std::collections::{BTreeSet, HashMap}; + +use solvecraft_geom::{Plane, Region2, Vec2, Vec3}; +use truck_modeling as mt; + +use super::{Index, Section, Tag, dist_to, inside}; +use crate::BoolOp; +use crate::body::{Body, edge_order, from_p3}; +use crate::history::{EdgeOrigin, FaceOrigin, FaceOrigin::Face, FaceProv}; + +/// A cap of an operand: a planar face square to the prism direction. +struct Cap { + origin: FaceOrigin, + /// Its level and whether it faces along the direction. + z: f64, + up: bool, + outline: Outline, +} + +/// A section's regions with the walls its outline was read beside and its segments' middles, +/// to tell whether two sections overlap. +struct Outline { + regions: Vec, + walls: BTreeSet<(usize, usize)>, + middles: Vec, +} + +impl Outline { + fn of(s: &Section) -> Outline { + let (segs, tags) = s.segments(); + Outline { + regions: s.regions.clone(), + walls: tags.iter().filter_map(|t| Some((t.operand, t.wall?))).collect(), + middles: segs.iter().map(|g| g.point_at(0.5)).collect(), + } + } +} + +/// What a prism boolean's result is built from: the operands (target, tool), the direction and +/// the plane square to it that sections are drawn in, and for simple prisms each operand's +/// extent along the direction. +pub(super) struct Context<'a> { + pub op: BoolOp, + pub operands: [&'a Body; 2], + pub index: [&'a Index; 2], + pub plane: Plane, + pub tol: f64, + pub heights: Option<[(f64, f64); 2]>, +} + +/// How one piece of the result was built. +pub(super) enum Built { + /// One body of `crate::extrude` of the section's one region between levels `lo` and `hi`. + Extruded { section: Section, lo: f64, hi: f64 }, + /// Sewn (`sew_connected_sections`) from these faces, the sewing saying which each face of + /// the piece was built from (`None`: it could not). + Sewn { specs: Vec, faces: Option> }, +} + +/// A face sewn into a stepped prism. +pub(super) enum Spec { + /// A wall over a section segment (tagged `tag`) from level `z0` up. `next` is the wall over + /// the next segment of its loop, which it meets at `at`. + Wall { tag: Tag, z0: f64, next: usize, at: Vec2 }, + /// A cap over `section` at level `z`, facing along the direction when `up`. + Cap { section: Section, z: f64, up: bool }, +} + +/// A face of a piece: the wall over a segment with this tag between two levels, or a cap. +#[derive(Clone, Copy)] +enum Kind { + Wall(Tag, f64, f64), + Cap(usize), +} + +impl Context<'_> { + /// `body`, made of `pieces` (their shells in order), with its history over the operands + /// (none where it is not known). + pub(super) fn attach>(&self, body: Body, pieces: &[(B, Built)]) -> Body { + let pieces: Vec<(&Body, &Built)> = pieces.iter().map(|(b, k)| (b.borrow(), k)).collect(); + let (faces, edges) = self.history(&pieces).unzip(); + crate::record::traced(body, &crate::record::counts(&self.operands), faces, edges.as_ref()) + } + + /// The faces (each a region of operand faces, and whether it is one untouched) and the edges + /// (by identity) of the pieces, in order; `None` where something is not known. + pub(super) fn history(&self, pieces: &[(&Body, &Built)]) -> Option<(Vec, HashMap>)> { + let caps = self.caps()?; + let mut faces = Vec::new(); + let mut edges = HashMap::new(); + for (body, built) in pieces { + let (f, e) = match built { + Built::Extruded { section, lo, hi } => self.extruded(&caps, body, section, *lo, *hi)?, + Built::Sewn { specs, faces } => self.sewn(&caps, body, specs, faces.as_deref()?)?, + }; + faces.extend(f); + edges.extend(e); + } + Some((crate::record::regions(faces), edges)) + } + + /// Both operands' caps, their outlines tagged. + fn caps(&self) -> Option> { + let d = self.plane.normal(); + let mut caps = Vec::new(); + let (mut work, mut segments) = (2_000_000usize, super::MAX_PLANAR_SEGMENTS * 2); + for (operand, (body, index)) in self.operands.iter().zip(self.index).enumerate() { + for (face, f) in body.solid.face_iter().enumerate() { + let mt::Surface::Plane(p) = f.oriented_surface() else { continue }; + let n = p.normal(); + let n = Vec3::new(n.x, n.y, n.z).normalized()?; + if n.dot(d).abs() < 1.0 - 1e-9 { + continue; + } + let section = Section::from_loops(index.loops(f, face, &self.plane, self.tol, &mut work, &mut segments)?)?; + let (z, up) = (self.plane.height(from_p3(p.origin())), n.dot(d) > 0.0); + caps.push(Cap { origin: Face { operand, face }, z, up, outline: Outline::of(§ion.oriented()) }); + } + } + Some(caps) + } + + /// The operand caps under a result cap over `section` at level `z` (facing along the + /// direction when `up`): the target's caps there facing the same way, and the tool's facing + /// the same way (a union or intersection) or the other way (a cut, whose tool's caps bound + /// what it leaves), wherever they overlap the section. + fn cap_origins(&self, caps: &[Cap], section: &Section, z: f64, up: bool) -> BTreeSet { + let outline = Outline::of(section); + caps.iter() + .filter(|c| { + let facing = match c.origin { + Face { operand: 1, .. } if self.op == BoolOp::Cut => c.up != up, + _ => c.up == up, + }; + facing && (c.z - z).abs() < self.tol && self.overlap(&outline, &c.outline) + }) + .map(|c| c.origin) + .collect() + } + + /// Do two sections (at one level) overlap? Where their outlines share a wall, or a point of + /// one's outline, clear of the other's, lies inside it. + fn overlap(&self, x: &Outline, y: &Outline) -> bool { + let within = |x: &Outline, y: &Outline| x.middles.iter().any(|m| dist_to(&y.regions, *m) > self.tol && inside(&y.regions, *m)); + !x.walls.is_disjoint(&y.walls) || within(x, y) || within(y, x) + } + + /// The operand wall a segment was read beside. + fn wall(t: Tag) -> Option { + Some(Face { operand: t.operand, face: t.wall? }) + } + + /// The operand's edges (by number) bounding exactly the faces `faces`, and touching vertex + /// `at` when given. + fn edges_between(&self, operand: usize, [f, g]: [usize; 2], at: Option) -> Vec { + let Some(index) = self.index.get(operand) else { return Vec::new() }; + let Some(between) = index.between.get(&(f.min(g), f.max(g))) else { return Vec::new() }; + between + .iter() + .copied() + .filter(|e| at.is_none_or(|v| index.bounds.get(*e).is_some_and(|(_, front, back)| *front == Some(v) || *back == Some(v)))) + .collect() + } + + /// Is the wall over a segment tagged `t` between `lo` and `hi` its operand wall untouched? + /// The whole cap edge it was read off, the operand's whole height (simple prisms), and a + /// wall with no other cap edges than its two. + fn whole(&self, caps: &[Cap], t: Tag, lo: f64, hi: f64) -> bool { + let (Some(heights), Some(wall), Some(_), Some(_)) = (self.heights, t.wall, t.start, t.end) else { return false }; + let Some((l, h)) = heights.get(t.operand) else { return false }; + let Some(index) = self.index.get(t.operand) else { return false }; + let cap_edges: usize = caps + .iter() + .filter_map(|c| match c.origin { + Face { operand, face } if operand == t.operand => index.between.get(&(face.min(wall), face.max(wall))).map(Vec::len), + _ => None, + }) + .sum(); + (lo - l).abs() < self.tol && (hi - h).abs() < self.tol && cap_edges == 2 + } + + /// The origins of the edge between the wall over a segment tagged `t` and a cap lying on the + /// operand faces `cap`: the operand's edge between the wall and its own cap there (the very + /// edge the segment was read off, when that is one), else where the wall meets the cap. + fn cap_edge(&self, t: Tag, cap: &BTreeSet) -> Option> { + let (x, w) = (t.operand, t.wall?); + let own: Vec = + cap.iter().filter_map(|o| if let Face { operand, face } = *o { (operand == x).then_some(face) } else { None }).collect(); + if let Some(e) = t.edge + && self.index.get(x)?.bounds.get(e).is_some_and(|(fs, _, _)| fs.contains(&w) && fs.iter().any(|f| own.contains(f))) + { + return Some(vec![EdgeOrigin::Edge { operand: x, edge: e }]); + } + let along: Vec = own.iter().flat_map(|c| self.edges_between(x, [w, *c], None)).collect(); + match (along.as_slice(), cap.first()) { + ([e], _) => Some(vec![EdgeOrigin::Edge { operand: x, edge: *e }]), + ([], Some(o)) if cap.len() == 1 => { + let wall = Self::wall(t)?; + Some(vec![EdgeOrigin::Meet { a: wall.min(*o), b: wall.max(*o) }]) + } + _ => None, + } + } + + /// The origins of the edge between the walls over two segments where the first (tagged + /// `t1`) runs into the second (`t2`): the operand's edge there between its walls (at the + /// operand vertex the segments meet at), where two operands' walls meet, or none within one + /// wall (a segment split there). + fn junction(&self, t1: Tag, t2: Tag) -> Option> { + let (p, q) = (Self::wall(t1)?, Self::wall(t2)?); + if t1.operand != t2.operand { + return Some(vec![EdgeOrigin::Meet { a: p.min(q), b: p.max(q) }]); + } + let (x, w1, w2) = (t1.operand, t1.wall?, t2.wall?); + match (t1.end, t2.start) { + (Some(v), Some(u)) if v == u => match self.edges_between(x, [w1, w2], Some(v)).as_slice() { + [e] => Some(vec![EdgeOrigin::Edge { operand: x, edge: *e }]), + _ => None, + }, + _ if w1 == w2 => Some(Vec::new()), + _ => None, + } + } + + /// The origins of the edge between walls over one segment stacked at a level (tagged `t1` + /// below, `t2` above): within one wall (none), along the operand's edge between two of its + /// walls, or where two operands' walls meet. + fn stacked(&self, t1: Tag, t2: Tag) -> Option> { + let (p, q) = (Self::wall(t1)?, Self::wall(t2)?); + if p == q { + return Some(Vec::new()); + } + if t1.operand != t2.operand { + return Some(vec![EdgeOrigin::Meet { a: p.min(q), b: p.max(q) }]); + } + match self.edges_between(t1.operand, [t1.wall?, t2.wall?], None).as_slice() { + [e] => Some(vec![EdgeOrigin::Edge { operand: t1.operand, edge: *e }]), + _ => None, + } + } + + /// Faces and edges of a piece `crate::extrude` built from a section of one region. Its + /// history says which faces are walls over which segment (numbered over the region's + /// loops in order) and which are its caps; the start cap's wires run over the segments in + /// loop order (as the extrude builds them), so the vertex after each wire edge is where one + /// segment runs into the next, or splits. + #[allow(clippy::type_complexity)] + fn extruded( + &self, + caps: &[Cap], + body: &Body, + section: &Section, + lo: f64, + hi: f64, + ) -> Option<(Vec<(BTreeSet, bool)>, HashMap>)> { + let h = body.history()?; + let tags = section.segments().1; + let tops = [self.cap_origins(caps, section, lo, false), self.cap_origins(caps, section, hi, true)]; + let kinds: Vec = h + .faces + .iter() + .map(|f| match f.origins.as_slice() { + [FaceOrigin::Profile { segment }] => Some(Kind::Wall(*tags.get(*segment)?, lo, hi)), + [FaceOrigin::Cap { end }] => Some(Kind::Cap(usize::from(*end))), + _ => None, + }) + .collect::>()?; + let faces = kinds + .iter() + .map(|k| match *k { + Kind::Wall(t, lo, hi) => Some((std::iter::once(Self::wall(t)?).collect(), self.whole(caps, t, lo, hi))), + Kind::Cap(end) => Some(tops.get(end).filter(|o| !o.is_empty())?.clone()).map(|o| (o, false)), + }) + .collect::>>()?; + let (edges, bounds) = edge_order(body.solid.face_iter()); + let position: HashMap = edges.iter().enumerate().map(|(i, e)| (e.id(), i)).collect(); + // The segment of the wall beside an edge of the start cap. + let segment_of = |e: &mt::Edge| -> Option { + bounds.get(*position.get(&e.id())?)?.iter().find_map(|f| match h.faces.get(*f)?.origins.as_slice() { + [FaceOrigin::Profile { segment }] => Some(*segment), + _ => None, + }) + }; + let start = kinds.iter().position(|k| matches!(k, Kind::Cap(0)))?; + let mut junctions: HashMap = HashMap::new(); + for w in body.solid.face_iter().nth(start)?.absolute_boundaries() { + let es: Vec = w.edge_iter().cloned().collect(); + let segs: Vec = es.iter().map(segment_of).collect::>()?; + if segs.windows(2).any(|p| p.first() > p.get(1)) { + return None; + } + for (k, e) in es.iter().enumerate() { + let (s, next) = (*segs.get(k)?, *segs.get((k + 1) % segs.len())?); + let (t, u) = (*tags.get(s)?, *tags.get(next)?); + // A segment the extrude split in two runs on through the vertex between. + let pair = if s != next || k + 1 == segs.len() { (t, u) } else { (Tag { end: None, ..t }, Tag { start: None, ..t }) }; + junctions.insert(e.back().id(), pair); + } + } + let mut origins = HashMap::new(); + for (e, fs) in edges.iter().zip(&bounds) { + let [f, g] = fs.as_slice() else { return None }; + let o = match (kinds.get(*f)?, kinds.get(*g)?) { + (Kind::Wall(t, ..), Kind::Cap(end)) | (Kind::Cap(end), Kind::Wall(t, ..)) => self.cap_edge(*t, tops.get(*end)?)?, + (Kind::Wall(..), Kind::Wall(..)) => { + let (t1, t2) = [e.absolute_front().id(), e.absolute_back().id()].iter().find_map(|v| junctions.get(v))?; + self.junction(*t1, *t2)? + } + _ => return None, + }; + origins.insert(e.id(), o); + } + Some((faces, origins)) + } + + /// Faces and edges of a piece sewn from `specs`: the sewing reports which spec each face was + /// built from. Walls meeting at a vertex of their loop (when two walls meet at both ends, + /// the edge's vertex says which); walls over one segment meeting at a level; walls meeting + /// caps. + #[allow(clippy::type_complexity)] + fn sewn( + &self, + caps: &[Cap], + body: &Body, + specs: &[Spec], + built: &[usize], + ) -> Option<(Vec<(BTreeSet, bool)>, HashMap>)> { + let cap_of: Vec>> = specs + .iter() + .map(|s| match s { + Spec::Cap { section, z, up } => Some(self.cap_origins(caps, section, *z, *up)).filter(|o| !o.is_empty()), + Spec::Wall { .. } => None, + }) + .collect(); + let faces = built + .iter() + .map(|i| match specs.get(*i)? { + Spec::Wall { tag, .. } => Some((std::iter::once(Self::wall(*tag)?).collect(), false)), + Spec::Cap { .. } => cap_of.get(*i)?.clone().map(|o| (o, false)), + }) + .collect::>>()?; + let (edges, bounds) = edge_order(body.solid.face_iter()); + let mut origins = HashMap::new(); + for (e, fs) in edges.iter().zip(&bounds) { + let [f, g] = fs.as_slice() else { return None }; + let (i, j) = (*built.get(*f)?, *built.get(*g)?); + let o = match (specs.get(i)?, specs.get(j)?) { + (Spec::Wall { tag, .. }, Spec::Cap { .. }) => self.cap_edge(*tag, cap_of.get(j)?.as_ref()?)?, + (Spec::Cap { .. }, Spec::Wall { tag, .. }) => self.cap_edge(*tag, cap_of.get(i)?.as_ref()?)?, + (Spec::Wall { tag: t, z0: a0, next: ni, at: pi, .. }, Spec::Wall { tag: u, z0: b0, next: nj, at: pj, .. }) => { + if (a0 - b0).abs() > self.tol { + // One over the other: the lower first. + if a0 < b0 { self.stacked(*t, *u)? } else { self.stacked(*u, *t)? } + } else { + // The vertex the edge stands on (in plan) says which way round they meet. + let p = self.plane.to_local(from_p3(e.absolute_front().point())); + let at = |q: &Vec2| p.dist(*q) < self.tol * 10.0; + if *ni == j && at(pi) { + self.junction(*t, *u)? + } else if *nj == i && at(pj) { + self.junction(*u, *t)? + } else { + return None; + } + } + } + _ => return None, + }; + origins.insert(e.id(), o); + } + Some((faces, origins)) + } +} diff --git a/crates/kernel/src/record.rs b/crates/kernel/src/record.rs index 957ac056..c0590db8 100644 --- a/crates/kernel/src/record.rs +++ b/crates/kernel/src/record.rs @@ -46,9 +46,98 @@ pub(crate) fn generated(origins: Vec) -> FaceProv { FaceProv { origins, change: Change::Generated } } -/// The index of each of `body`'s edges by identity, in its edge order. -fn edge_index(body: &Body) -> HashMap { - edge_order(body.solid.face_iter()).0.iter().enumerate().map(|(i, e)| (e.id(), i)).collect() +/// Result faces that are each a region of input faces, given as the input faces under it and +/// whether it is one input face untouched (`whole`). Kept when whole; Merged with several +/// origins; Split when its one origin lies under other result faces too; Modified otherwise; +/// Generated with no origin. +pub(crate) fn regions(faces: Vec<(BTreeSet, bool)>) -> Vec { + let mut faces: Vec = faces + .into_iter() + .map(|(origins, whole)| { + let change = match origins.len() { + 0 => Change::Generated, + 1 if whole => Change::Kept, + 1 => Change::Modified, + _ => Change::Merged, + }; + FaceProv { origins: origins.into_iter().collect(), change } + }) + .collect(); + mark_shared_split(&mut faces); + faces +} + +/// Faces of one origin that is under other faces too, and that still say Kept or Modified, +/// are Split (also where histories are put side by side: pieces of one body can share a +/// source). +pub(crate) fn mark_shared_split(faces: &mut [FaceProv]) { + let mut uses: HashMap = HashMap::new(); + for o in faces.iter().flat_map(|f| &f.origins) { + *uses.entry(*o).or_default() += 1; + } + for f in faces.iter_mut() { + if matches!(f.change, Change::Kept | Change::Modified) + && let [o] = f.origins.as_slice() + && uses.get(o).copied().unwrap_or(0) > 1 + { + f.change = Change::Split; + } + } +} + +/// The index of each of `solid`'s edges by identity, in its edge order. +pub(crate) fn edge_index(solid: &crate::body::Solid) -> HashMap { + edge_order(solid.face_iter()).0.iter().enumerate().map(|(i, e)| (e.id(), i)).collect() +} + +/// The index of each of `solid`'s faces by identity, in its face order. +pub(crate) fn face_index(solid: &crate::body::Solid) -> HashMap { + solid.face_iter().enumerate().map(|(i, f)| (f.id(), i)).collect() +} + +/// Each of `solid`'s edges, by identity, as edge `i` (its position in the solid's edge order) of +/// operand `operand`: the operand's own edges, or a copy's made by `Body::deep_copy` or truck's +/// mapping builders (`translated`, `transformed`), which build the same faces, wires and edges +/// in the same order, so position `i` of the copy is position `i` of the body it copies. +pub(crate) fn edge_origins(solid: &crate::body::Solid, operand: usize) -> HashMap> { + edge_order(solid.face_iter()).0.iter().enumerate().map(|(edge, e)| (e.id(), vec![EdgeOrigin::Edge { operand, edge }])).collect() +} + +/// The edge entries of the history of `body`, each edge's origins from `origins` (by identity); +/// `None` when an edge has none there. +pub(crate) fn edge_provs(body: &Body, origins: &HashMap>) -> Option> { + let (edges, bounds) = edge_order(body.solid.face_iter()); + edges.iter().zip(bounds).map(|(e, faces)| Some(EdgeProv { faces, origins: origins.get(&e.id())?.clone() })).collect() +} + +/// `body` with the history whose faces are `faces` (in its face order) and whose edges' +/// origins are `edges` (by identity), made from operands with the face and edge `counts`; with +/// no history where either is missing, an edge has no entry, or the history does not describe +/// the body (an operation whose history could not be recorded still gives its result). +pub(crate) fn traced( + body: Body, + counts: &[(usize, usize)], + faces: Option>, + edges: Option<&HashMap>>, +) -> Body { + let history = faces.zip(edges).and_then(|(faces, edges)| Some(History { operands: counts.len(), faces, edges: edge_provs(&body, edges)? })); + body.with_checked_history(history, counts) +} + +/// [`traced`] of the body `Body::new` makes of `solid`: faces given in `solid`'s face order +/// and edges by identity, both of which `Body::new` keeps. +pub(crate) fn build( + solid: crate::body::Solid, + counts: &[(usize, usize)], + faces: Option>, + edges: Option<&HashMap>>, +) -> Result { + Ok(traced(Body::new(solid)?, counts, faces, edges)) +} + +/// The face and edge counts of `bodies`, for [`traced`]. +pub(crate) fn counts(bodies: &[&Body]) -> Vec<(usize, usize)> { + bodies.iter().map(|b| (b.face_count(), b.edge_count())).collect() } /// An operation's operand, with its edges indexed by identity (in its edge order). @@ -59,7 +148,7 @@ pub(crate) struct Operand<'a> { impl<'a> Operand<'a> { pub(crate) fn new(body: &'a Body) -> Operand<'a> { - Operand { body, edges: edge_index(body) } + Operand { body, edges: edge_index(&body.solid) } } } diff --git a/crates/kernel/src/sew.rs b/crates/kernel/src/sew.rs index 7c2b6692..5e799e77 100644 --- a/crates/kernel/src/sew.rs +++ b/crates/kernel/src/sew.rs @@ -97,12 +97,14 @@ impl Cache { /// Sew the faces into a closed solid. pub fn sew(faces: &[FaceSpec]) -> Result { - sew_impl(faces, false, None) + sew_impl(faces, false, None).map(|(body, _)| body) } /// Separate boundary sheets at a shared geometric edge without moving it. /// Optional transient sources follow only unchanged produced face identities. -pub(crate) fn sew_touching_with_sources(faces: &[FaceSpec], sources: &[Option]) -> Result { +/// Also reports the face each face of the body was built from (by its place in `faces`; `None` +/// when that could not be followed). +pub(crate) fn sew_touching_with_sources(faces: &[FaceSpec], sources: &[Option]) -> Result<(Body, Option>)> { if sources.len() != faces.len() { return Err(KernelError::Invalid("sew source alignment".into())); } @@ -350,7 +352,7 @@ fn split_vertex_fans(faces: Vec) -> Result> { .collect() } -fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option]>) -> Result { +fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option]>) -> Result<(Body, Option>)> { if faces.len() < 4 || faces.len() > 200_000 { return Err(KernelError::Invalid("a solid needs 4…200000 faces".into())); } @@ -411,12 +413,15 @@ fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option = out.iter().enumerate().map(|(i, f)| (f.id(), i)).collect(); let shells = closed_components(out)?; let solid = Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("sew: the faces do not close: {e}")))?; + let from = solid.face_iter().map(|f| built.get(&f.id()).copied()).collect(); let body = Body::new(solid)?; let mapped = body.solid.face_iter().map(|f| source_ids.get(&f.id()).filter(|(o, _)| *o == f.orientation()).map(|(_, s)| (*s).clone())).collect(); - Ok(body.with_face_sources(sources.map(|_| std::sync::Arc::new(mapped)))) + Ok((body.with_face_sources(sources.map(|_| std::sync::Arc::new(mapped))), from)) }) } diff --git a/vendor/README.md b/vendor/README.md index 0f5236f5..e0ec2d0b 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -26,7 +26,8 @@ Changes: whose intersection made it. The code that creates faces and edges records them as it goes: `divide_face` (face pieces), `loops_store` (intersection edges, split edges, edges rebuilt on a moved vertex), across the passes that fold in the operands' extra shells. `and` / `or` are - wrappers that drop the history. + wrappers that drop the history; the `*_with_history` forms return the same solid with the + history as an `Option` (`None` where a face or edge went unrecorded). ## truck-stepio 0.3.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck) diff --git a/vendor/truck-shapeops/src/transversal/integrate/mod.rs b/vendor/truck-shapeops/src/transversal/integrate/mod.rs index 4745ab5d..7f64a2dd 100644 --- a/vendor/truck-shapeops/src/transversal/integrate/mod.rs +++ b/vendor/truck-shapeops/src/transversal/integrate/mod.rs @@ -124,27 +124,27 @@ pub fn or, S: ShapeOpsSurface>( } /// SolveCraft: AND operation between two solids, with where each face and edge of the result -/// came from (operand 0 is `solid0`, operand 1 is `solid1`). The operands must not share faces -/// or edges. +/// came from (operand 0 is `solid0`, operand 1 is `solid1`): the same result as [`and`], and +/// its history, `None` when a face or edge of it was not recorded. The operands must not share +/// faces or edges. pub fn and_with_history, S: ShapeOpsSurface>( solid0: &Solid, solid1: &Solid, tol: f64, -) -> Option<(Solid, History)> { - let (solid, history) = boolean(solid0, solid1, tol, |[and, _]| and)?; - Some((solid, history?)) +) -> Option<(Solid, Option)> { + boolean(solid0, solid1, tol, |[and, _]| and) } /// SolveCraft: OR operation between two solids, with where each face and edge of the result -/// came from (operand 0 is `solid0`, operand 1 is `solid1`). The operands must not share faces -/// or edges. +/// came from (operand 0 is `solid0`, operand 1 is `solid1`): the same result as [`or`], and +/// its history, `None` when a face or edge of it was not recorded. The operands must not share +/// faces or edges. pub fn or_with_history, S: ShapeOpsSurface>( solid0: &Solid, solid1: &Solid, tol: f64, -) -> Option<(Solid, History)> { - let (solid, history) = boolean(solid0, solid1, tol, |[_, or]| or)?; - Some((solid, history?)) +) -> Option<(Solid, Option)> { + boolean(solid0, solid1, tol, |[_, or]| or) } /// Combines the operands' first shells, then folds in their other shells one at a time diff --git a/vendor/truck-shapeops/src/transversal/integrate/tests.rs b/vendor/truck-shapeops/src/transversal/integrate/tests.rs index 86797b88..2e033ad5 100644 --- a/vendor/truck-shapeops/src/transversal/integrate/tests.rs +++ b/vendor/truck-shapeops/src/transversal/integrate/tests.rs @@ -85,6 +85,7 @@ fn box_union_box_history() { let a = cuboid(Point3::origin(), Vector3::new(1.0, 1.0, 1.0)); let b = cuboid(Point3::new(0.5, 0.25, 0.25), Vector3::new(1.0, 0.5, 0.5)); let (or, history) = crate::or_with_history(&a, &b, 0.05).unwrap(); + let history = history.unwrap(); let (kept, meets) = check(&or, [&a, &b], &history, 0.05); // a's five faces away from b and b's far end are kept; a's face at x = 1 gets a hole and // b's four sides are trimmed where they enter a. @@ -111,6 +112,7 @@ fn box_minus_cylinder_history() { let mut cylinder = builder::tsweep(&f, Vector3::unit_z() * 2.0); cylinder.not(); let (cut, history) = crate::and_with_history(&cube, &cylinder, 0.05).unwrap(); + let history = history.unwrap(); let (kept, meets) = check(&cut, [&cube, &cylinder], &history, 0.05); // The cube's four sides are kept; its top and bottom get holes; only the cylinder's wall // remains of the tool, trimmed to the cube. @@ -137,6 +139,7 @@ fn multi_shell_operand_history() { // Covers the upper half of the hollow cube: its bottom crosses both shells. let slab = cuboid(Point3::new(-0.5, -0.5, 1.5), Vector3::new(4.0, 4.0, 2.5)); let (and, history) = crate::and_with_history(&hollow, &slab, 0.05).unwrap(); + let history = history.unwrap(); let (kept, meets) = check(&and, [&hollow, &slab], &history, 0.05); // The outer top and the void's top (numbered after the outer shell's six faces) are kept. assert_eq!(kept, 2);