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);