diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index f4ae45e2..d59cf86e 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -76,7 +76,7 @@ Generated-in-code assets are original and have no file to list: |---|---|---|---| | vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: parameter searches kept inside the surface domain (vendor/README.md) | | vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) | -| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; trace macro (vendor/README.md) | +| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro (vendor/README.md) | | vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) | ## Vendored renderer compatibility fix diff --git a/vendor/README.md b/vendor/README.md index d9068532..0f5236f5 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -19,6 +19,14 @@ Changes: boolean's faces came out in a different order on every run). - `lib.rs`: `tr!` reports where an operation gives up when `SHAPEOPS_TRACE` is set; compiler warnings no longer fail the build. +- Face and edge history (`transversal/history.rs`, new): `and_with_history` / `or_with_history` + also return, for each face of the result (in `face_iter` order), the operand face it is a + region of and whether it is that face unchanged, and for each edge (distinct edges in order + of first use along the faces) the operand edge it is (a piece of) or the two operand faces + 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. ## truck-stepio 0.3.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck) diff --git a/vendor/truck-shapeops/src/lib.rs b/vendor/truck-shapeops/src/lib.rs index 3dbb1a86..89277a76 100644 --- a/vendor/truck-shapeops/src/lib.rs +++ b/vendor/truck-shapeops/src/lib.rs @@ -33,7 +33,10 @@ pub(crate) use tr; mod healing; pub use healing::{RobustSplitClosedEdgesAndFaces, SplitClosedEdgesAndFaces}; mod transversal; -pub use transversal::{and, or, ShapeOpsCurve, ShapeOpsSurface}; +pub use transversal::{ + and, and_with_history, or, or_with_history, EdgeOrigin, FaceIndex, FaceOrigin, History, ShapeOpsCurve, + ShapeOpsSurface, +}; // SolveCraft: exercised by kernel regressions in the workspace CI. #[doc(hidden)] pub use transversal::IntersectionCurveWithParameters; diff --git a/vendor/truck-shapeops/src/transversal/divide_face/mod.rs b/vendor/truck-shapeops/src/transversal/divide_face/mod.rs index 965599ce..4b1b1761 100644 --- a/vendor/truck-shapeops/src/transversal/divide_face/mod.rs +++ b/vendor/truck-shapeops/src/transversal/divide_face/mod.rs @@ -1,6 +1,7 @@ #![allow(clippy::many_single_char_names)] use super::faces_classification::FacesClassification; +use super::history::Lineage; use super::loops_store::*; use rustc_hash::FxHashMap as HashMap; use std::ops::Deref; @@ -128,6 +129,7 @@ pub fn divide_faces( shell: &Shell, loops_store: &LoopsStore, tol: f64, + lineage: &mut Lineage, ) -> Option> where C: BoundedCurve + ParameterDivision1D, @@ -145,8 +147,10 @@ where res.push(face.clone(), ShapesOpStatus::Unknown); } else { let vec = crate::tr!(divide_one_face(face, loops, tol), "divide one face"); - vec.into_iter() - .for_each(|(face, status)| res.push(face, status)); + vec.into_iter().for_each(|(piece, status)| { + lineage.face_piece(face.id(), piece.id()); + res.push(piece, status) + }); } Some(()) })?; diff --git a/vendor/truck-shapeops/src/transversal/divide_face/tests.rs b/vendor/truck-shapeops/src/transversal/divide_face/tests.rs index 7856330d..47069080 100644 --- a/vendor/truck-shapeops/src/transversal/divide_face/tests.rs +++ b/vendor/truck-shapeops/src/transversal/divide_face/tests.rs @@ -198,11 +198,11 @@ fn independent_intersection() { geom_loops_store0: loops_store0, geom_loops_store1: loops_store1, .. - } = create_loops_stores(&shell0, &poly_shell0, &shell1, &poly_shell1, TOL).unwrap(); - let [and0, or0, unknown0] = divide_faces(&shell0, &loops_store0, TOL) + } = create_loops_stores(&shell0, &poly_shell0, &shell1, &poly_shell1, TOL, &mut Default::default()).unwrap(); + let [and0, or0, unknown0] = divide_faces(&shell0, &loops_store0, TOL, &mut Default::default()) .unwrap() .and_or_unknown(); - let [and1, or1, unknown1] = divide_faces(&shell1, &loops_store1, TOL) + let [and1, or1, unknown1] = divide_faces(&shell1, &loops_store1, TOL, &mut Default::default()) .unwrap() .and_or_unknown(); assert_eq!(and0.len(), 1); diff --git a/vendor/truck-shapeops/src/transversal/faces_classification/tests.rs b/vendor/truck-shapeops/src/transversal/faces_classification/tests.rs index a839602d..0f8d143d 100644 --- a/vendor/truck-shapeops/src/transversal/faces_classification/tests.rs +++ b/vendor/truck-shapeops/src/transversal/faces_classification/tests.rs @@ -112,11 +112,11 @@ fn independent_intersection() { geom_loops_store0: loops_store0, geom_loops_store1: loops_store1, .. - } = loops_store::create_loops_stores(&shell0, &poly_shell0, &shell1, &poly_shell1, TOL) + } = loops_store::create_loops_stores(&shell0, &poly_shell0, &shell1, &poly_shell1, TOL, &mut Default::default()) .unwrap(); - let mut cls0 = divide_face::divide_faces(&shell0, &loops_store0, TOL).unwrap(); + let mut cls0 = divide_face::divide_faces(&shell0, &loops_store0, TOL, &mut Default::default()).unwrap(); cls0.integrate_by_component(); - let mut cls1 = divide_face::divide_faces(&shell1, &loops_store1, TOL).unwrap(); + let mut cls1 = divide_face::divide_faces(&shell1, &loops_store1, TOL, &mut Default::default()).unwrap(); cls1.integrate_by_component(); let [mut and, mut or, _] = cls0.and_or_unknown(); diff --git a/vendor/truck-shapeops/src/transversal/history.rs b/vendor/truck-shapeops/src/transversal/history.rs new file mode 100644 index 00000000..33dadcba --- /dev/null +++ b/vendor/truck-shapeops/src/transversal/history.rs @@ -0,0 +1,133 @@ +//! SolveCraft: where each face and edge of a boolean's result came from, recorded by the code +//! that builds them (never reconstructed from geometry). +//! +//! Topology ids are addresses of shared data, so they identify a face or an edge only while it +//! lives. [`Lineage`] therefore stores, for every face and edge an operation creates, its origin +//! in terms of the operands at the moment it is created (while its parent is alive), and never a +//! chain of ids: a dead entity's address that is reused is overwritten when the new entity is +//! recorded. + +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; +use truck_topology::*; + +/// A face of one of a boolean's two operands. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FaceIndex { + /// 0 for the first operand, 1 for the second. + pub operand: usize, + /// Position in that operand's `face_iter` order. + pub face: usize, +} + +/// Where a face of a boolean's result lies. Every result face is a region of exactly one +/// operand face (a boolean makes no face of its own). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct FaceOrigin { + /// The operand face it is a region of (same surface, same orientation). + pub source: FaceIndex, + /// `true` when it is that very face, passed through untouched (no intersection reached + /// it); `false` for a region rebuilt from it by the face division. + pub kept: bool, +} + +/// Where an edge of a boolean's result came from. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum EdgeOrigin { + /// An operand edge or a piece of it. + Edge { + /// 0 for the first operand, 1 for the second. + operand: usize, + /// Position in that operand's edge order (see [`History`]). + edge: usize, + }, + /// The intersection of two operand faces, or a piece of it (`a < b`). + Meet { + /// The lesser of the two faces. + a: FaceIndex, + /// The greater of the two faces. + b: FaceIndex, + }, +} + +/// History of a boolean's result: one entry per face in its `face_iter` order and one per edge +/// in its edge order. A solid's edge order lists its distinct edges in order of first use, +/// walking `face_iter` and each face's `edge_iter` (that is, `Solid::edge_iter` without repeats). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct History { + /// Origin of each result face. + pub faces: Vec, + /// Origin of each result edge. + pub edges: Vec, +} + +/// The ids of `solid`'s edges in its edge order (see [`History`]). +fn edge_order(solid: &Solid) -> Vec> { + let mut seen = HashSet::default(); + solid.edge_iter().map(|e| e.id()).filter(|id| seen.insert(*id)).collect() +} + +/// Records `origin` under `key`, or, when the origin is unknown, leaves nothing stale under the +/// new entity's address. +fn set(map: &mut HashMap, key: K, origin: Option) { + match origin { + Some(o) => map.insert(key, o), + None => map.remove(&key), + }; +} + +/// Origins, in operand terms, of the faces and edges alive during one boolean. +/// +/// The operands must not share faces or edges (a shared one is attributed to the second). +#[derive(Debug)] +pub(crate) struct Lineage { + faces: HashMap, FaceOrigin>, + edges: HashMap, EdgeOrigin>, +} + +impl Default for Lineage { + fn default() -> Self { Self { faces: HashMap::default(), edges: HashMap::default() } } +} + +impl Lineage { + /// Every face and edge of the operands, as itself. + pub fn new

(operands: [&Solid; 2]) -> Self { + let mut lineage = Self::default(); + for (operand, solid) in operands.into_iter().enumerate() { + for (face, f) in solid.face_iter().enumerate() { + let source = FaceIndex { operand, face }; + lineage.faces.insert(f.id(), FaceOrigin { source, kept: true }); + } + for (edge, id) in edge_order(solid).into_iter().enumerate() { + lineage.edges.insert(id, EdgeOrigin::Edge { operand, edge }); + } + } + lineage + } + + /// `piece` is a region trimmed out of the live face `parent`. + pub fn face_piece(&mut self, parent: FaceID, piece: FaceID) { + let origin = self.faces.get(&parent).map(|&o| FaceOrigin { kept: false, ..o }); + set(&mut self.faces, piece, origin); + } + + /// `piece` is the live edge `parent` or a piece of it. + pub fn edge_piece(&mut self, parent: EdgeID, piece: EdgeID) { + let origin = self.edges.get(&parent).copied(); + set(&mut self.edges, piece, origin); + } + + /// `edge` is (part of) the intersection of the live faces `faces`. + pub fn meet(&mut self, edge: EdgeID, faces: [FaceID; 2]) { + let [a, b] = faces.map(|f| self.faces.get(&f).map(|o| o.source)); + let origin = a.zip(b).map(|(a, b)| EdgeOrigin::Meet { a: a.min(b), b: a.max(b) }); + set(&mut self.edges, edge, origin); + } + + /// The history of `solid`, made from the operands by the recorded steps; `None` when one + /// of its faces or edges was not recorded. + pub fn history

(&self, solid: &Solid) -> Option { + let faces = solid.face_iter().map(|f| self.faces.get(&f.id()).copied()).collect::>(); + let edges = edge_order(solid).into_iter().map(|id| self.edges.get(&id).copied()).collect::>(); + Some(History { faces: crate::tr!(faces, "face history"), edges: crate::tr!(edges, "edge history") }) + } +} diff --git a/vendor/truck-shapeops/src/transversal/integrate/mod.rs b/vendor/truck-shapeops/src/transversal/integrate/mod.rs index 83b2ce0c..4745ab5d 100644 --- a/vendor/truck-shapeops/src/transversal/integrate/mod.rs +++ b/vendor/truck-shapeops/src/transversal/integrate/mod.rs @@ -1,3 +1,4 @@ +use super::history::{History, Lineage}; use super::*; use truck_geometry::prelude::IntersectionCurve; use truck_meshalgo::prelude::*; @@ -51,6 +52,7 @@ fn process_one_pair_of_shells, S: ShapeOpsSurface>( shell0: &Shell, shell1: &Shell, tol: f64, + lineage: &mut Lineage, ) -> Option<[Shell; 2]> { nonpositive_tolerance!(tol); // SolveCraft: robust meshing (boundaries from earlier operations lie on surfaces only @@ -61,10 +63,10 @@ fn process_one_pair_of_shells, S: ShapeOpsSurface>( geom_loops_store0: loops_store0, geom_loops_store1: loops_store1, .. - } = crate::tr!(loops_store::create_loops_stores(shell0, &poly_shell0, shell1, &poly_shell1, tol), "loops stores"); - let mut cls0 = crate::tr!(divide_face::divide_faces(shell0, &loops_store0, tol), "divide faces 0"); + } = crate::tr!(loops_store::create_loops_stores(shell0, &poly_shell0, shell1, &poly_shell1, tol, lineage), "loops stores"); + let mut cls0 = crate::tr!(divide_face::divide_faces(shell0, &loops_store0, tol, lineage), "divide faces 0"); cls0.integrate_by_component(); - let mut cls1 = crate::tr!(divide_face::divide_faces(shell1, &loops_store1, tol), "divide faces 1"); + let mut cls1 = crate::tr!(divide_face::divide_faces(shell1, &loops_store1, tol, lineage), "divide faces 1"); cls1.integrate_by_component(); let [mut and0, mut or0, unknown0] = cls0.and_or_unknown(); unknown0.into_iter().try_for_each(|face| { @@ -109,21 +111,7 @@ pub fn and, S: ShapeOpsSurface>( solid1: &Solid, tol: f64, ) -> Option> { - let mut iter0 = solid0.boundaries().iter(); - let mut iter1 = solid1.boundaries().iter(); - let shell0 = iter0.next()?; - let shell1 = iter1.next()?; - let [mut and_shell, _] = process_one_pair_of_shells(shell0, shell1, tol)?; - for shell in iter0 { - let [res, _] = process_one_pair_of_shells(&and_shell, shell, tol)?; - and_shell = res; - } - for shell in iter1 { - let [res, _] = process_one_pair_of_shells(&and_shell, shell, tol)?; - and_shell = res; - } - let boundaries = components_in_order(&and_shell); - Solid::try_new(boundaries).ok() + boolean(solid0, solid1, tol, |[and, _]| and).map(|(solid, _)| solid) } /// OR operation between two solids. @@ -132,21 +120,54 @@ pub fn or, S: ShapeOpsSurface>( solid1: &Solid, tol: f64, ) -> Option> { + boolean(solid0, solid1, tol, |[_, or]| or).map(|(solid, _)| solid) +} + +/// 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. +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?)) +} + +/// 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. +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?)) +} + +/// Combines the operands' first shells, then folds in their other shells one at a time +/// (`pick` chooses the AND or the OR shell of each pass). The history is `None` when a face or +/// edge of the result was not recorded; the result stands without it. +fn boolean, S: ShapeOpsSurface>( + solid0: &Solid, + solid1: &Solid, + tol: f64, + pick: fn([Shell; 2]) -> Shell, +) -> Option<(Solid, Option)> { + let mut lineage = Lineage::new([solid0, solid1]); let mut iter0 = solid0.boundaries().iter(); let mut iter1 = solid1.boundaries().iter(); let shell0 = iter0.next()?; let shell1 = iter1.next()?; - let [_, mut or_shell] = process_one_pair_of_shells(shell0, shell1, tol)?; - for shell in iter0 { - let [_, res] = process_one_pair_of_shells(&or_shell, shell, tol)?; - or_shell = res; + let mut shell = pick(crate::tr!(process_one_pair_of_shells(shell0, shell1, tol, &mut lineage), "first shells")); + for other in iter0.chain(iter1) { + shell = pick(crate::tr!(process_one_pair_of_shells(&shell, other, tol, &mut lineage), "another shell")); } - for shell in iter1 { - let [_, res] = process_one_pair_of_shells(&or_shell, shell, tol)?; - or_shell = res; - } - let boundaries = components_in_order(&or_shell); - Solid::try_new(boundaries).ok() + let solid = crate::tr!(Solid::try_new(components_in_order(&shell)).ok(), "result solid"); + let history = lineage.history(&solid); + Some((solid, history)) } // SolveCraft: truck's connected components come out in hash order of face addresses, so the diff --git a/vendor/truck-shapeops/src/transversal/integrate/tests.rs b/vendor/truck-shapeops/src/transversal/integrate/tests.rs index b59d87ef..86797b88 100644 --- a/vendor/truck-shapeops/src/transversal/integrate/tests.rs +++ b/vendor/truck-shapeops/src/transversal/integrate/tests.rs @@ -19,3 +19,132 @@ fn punched_cube() { let file = std::fs::File::create("punched-cube.obj").unwrap(); obj::write(&poly, file).unwrap(); } + +// SolveCraft: boolean history. + +use crate::{EdgeOrigin, FaceIndex, History}; + +fn cuboid(origin: Point3, size: Vector3) -> Solid { + let v = builder::vertex(origin); + let e = builder::tsweep(&v, size.x * Vector3::unit_x()); + let f = builder::tsweep(&e, size.y * Vector3::unit_y()); + builder::tsweep(&f, size.z * Vector3::unit_z()) +} + +/// Distinct edges in order of first use along the faces. +fn edges_in_order(solid: &Solid) -> Vec { + let mut seen = std::collections::HashSet::new(); + solid.face_iter().flat_map(Face::edge_iter).filter(|e| seen.insert(e.id())).collect() +} + +fn on_surface(surface: &Surface, pt: Point3, tol: f64) -> bool { + surface.search_nearest_parameter(pt, None, 100).is_some_and(|(u, v)| surface.subs(u, v).distance(pt) < tol) +} + +fn on_curve(curve: &Curve, pt: Point3, tol: f64) -> bool { + curve.search_nearest_parameter(pt, None, 100).is_some_and(|t| curve.subs(t).distance(pt) < tol) +} + +/// Checks `history` against the geometry of `result` and the operands; returns how many result +/// faces are kept operand faces and the intersection edges. +fn check(result: &Solid, operands: [&Solid; 2], history: &History, tol: f64) -> (usize, Vec<[FaceIndex; 2]>) { + let operand_faces = operands.map(|s| s.face_iter().collect::>()); + let face = |f: FaceIndex| operand_faces[f.operand][f.face]; + assert_eq!(history.faces.len(), result.face_iter().count()); + for (out, origin) in result.face_iter().zip(&history.faces) { + let src = face(origin.source); + assert_eq!(out.orientation(), src.orientation()); + assert_eq!(out.id() == src.id(), origin.kept); + assert!(out.vertex_iter().all(|v| on_surface(&src.surface(), v.point(), tol))); + } + let edges = edges_in_order(result); + assert_eq!(history.edges.len(), edges.len()); + let operand_edges = operands.map(edges_in_order); + let mut meets = Vec::new(); + for (out, origin) in edges.iter().zip(&history.edges) { + let ends = [out.front().point(), out.back().point()]; + match *origin { + EdgeOrigin::Edge { operand, edge } => { + let src = &operand_edges[operand][edge]; + assert!(ends.iter().all(|&p| on_curve(&src.curve(), p, tol))); + } + EdgeOrigin::Meet { a, b } => { + assert!(a < b); + for f in [a, b] { + assert!(ends.iter().all(|&p| on_surface(&face(f).surface(), p, tol))); + } + meets.push([a, b]); + } + } + } + (history.faces.iter().filter(|o| o.kept).count(), meets) +} + +#[test] +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 (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. + assert_eq!(history.faces.len(), 11); + assert_eq!(kept, 6); + let pieces: Vec<_> = history.faces.iter().filter(|o| !o.kept).map(|o| o.source).collect(); + assert_eq!(pieces.iter().filter(|f| f.operand == 0).count(), 1); + assert_eq!(pieces.iter().filter(|f| f.operand == 1).count(), 4); + // b's sides meet a's face at x = 1 along four new edges. + assert_eq!(meets.len(), 4); + assert!(meets.iter().all(|[x, y]| x.operand == 0 && y.operand == 1 && *x == pieces[0])); + // a's 12 edges, b's 4 far-end edges and the 4 trimmed edges along x. + assert_eq!(history.edges.len(), 24); + let from_b = history.edges.iter().filter(|o| matches!(o, EdgeOrigin::Edge { operand: 1, .. })).count(); + assert_eq!(from_b, 8); +} + +#[test] +fn box_minus_cylinder_history() { + let cube = cuboid(Point3::origin(), Vector3::new(1.0, 1.0, 1.0)); + let v = builder::vertex(Point3::new(0.5, 0.25, -0.5)); + let w = builder::rsweep(&v, Point3::new(0.5, 0.5, 0.0), Vector3::unit_z(), Rad(7.0)); + let f = builder::try_attach_plane(&[w]).unwrap(); + 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 (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. + assert_eq!(kept, 4); + assert!(history.faces.iter().filter(|o| o.source.operand == 1).all(|o| !o.kept)); + // The holes' rims: the wall meets the top and the bottom. + assert!(!meets.is_empty()); + assert!(meets.iter().all(|[x, y]| x.operand == 0 && y.operand == 1)); + let rims: std::collections::BTreeSet<_> = meets.iter().map(|[x, _]| *x).collect(); + assert_eq!(rims.len(), 2); +} + +/// A cube [0, 3]³ with a cubic void [1, 2]³: two shells, the void's faces after the outer ones. +fn hollow_cube() -> Solid { + let mut void = cuboid(Point3::new(1.0, 1.0, 1.0), Vector3::new(1.0, 1.0, 1.0)); + void.not(); + let outer = cuboid(Point3::origin(), Vector3::new(3.0, 3.0, 3.0)); + Solid::new(outer.into_boundaries().into_iter().chain(void.into_boundaries()).collect()) +} + +#[test] +fn multi_shell_operand_history() { + let hollow = hollow_cube(); + // 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 (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); + assert!(history.faces.iter().any(|o| o.kept && o.source.operand == 0 && o.source.face >= 6)); + // The slab's bottom meets the four outer sides and the void's four sides. + let outer = meets.iter().filter(|[x, _]| x.operand == 0 && x.face < 6).count(); + let void = meets.iter().filter(|[x, _]| x.operand == 0 && x.face >= 6).count(); + assert_eq!((outer, void), (4, 4), "{meets:?}"); + let slab_faces: std::collections::BTreeSet<_> = meets.iter().map(|[_, y]| *y).collect(); + assert_eq!(slab_faces.len(), 1); +} diff --git a/vendor/truck-shapeops/src/transversal/loops_store/mod.rs b/vendor/truck-shapeops/src/transversal/loops_store/mod.rs index 7958997e..a8339675 100644 --- a/vendor/truck-shapeops/src/transversal/loops_store/mod.rs +++ b/vendor/truck-shapeops/src/transversal/loops_store/mod.rs @@ -1,6 +1,7 @@ #![allow(clippy::many_single_char_names)] use super::*; +use super::history::Lineage; use rustc_hash::FxHashMap as HashMap; use truck_base::cgmath64::*; use truck_geometry::prelude::IntersectionCurve; @@ -172,11 +173,13 @@ impl Loops { }) } + // SolveCraft: `replaced(old, new)` is told of every edge rebuilt on the new vertex. fn change_vertex( &mut self, old_vertex: &Vertex

, new_vertex: &Vertex

, emap: &mut HashMap, Edge>, + replaced: &mut impl FnMut(EdgeID, EdgeID), ) { self.iter_mut() .flat_map(|wire| wire.iter_mut()) @@ -193,6 +196,7 @@ impl Loops { return; } .clone(); + replaced(edge.id(), new_edge.id()); if !edge.orientation() { new_edge.invert(); } @@ -290,9 +294,10 @@ impl LoopsStore { old_vertex: &Vertex

, new_vertex: &Vertex

, emap: &mut HashMap, Edge>, + replaced: &mut impl FnMut(EdgeID, EdgeID), ) { self.iter_mut() - .for_each(|loops| loops.change_vertex(old_vertex, new_vertex, emap)); + .for_each(|loops| loops.change_vertex(old_vertex, new_vertex, emap, replaced)); } #[inline(always)] @@ -317,13 +322,13 @@ impl LoopsStore { let old_vertex = self[loops_index][wire_index][edge_index] .absolute_front() .clone(); - self.change_vertex(&old_vertex, v, emap); + self.change_vertex(&old_vertex, v, emap, &mut |_, _| {}); } ParameterKind::Back => { let old_vertex = self[loops_index][wire_index][edge_index] .absolute_back() .clone(); - self.change_vertex(&old_vertex, v, emap); + self.change_vertex(&old_vertex, v, emap, &mut |_, _| {}); } ParameterKind::Inner(t) => { let edge = self[loops_index][wire_index][edge_index].absolute_clone(); @@ -345,6 +350,7 @@ impl LoopsStore { kind: ParameterKind, another_surface: &S, emap: &mut HashMap, Edge>, + lineage: &mut Lineage, ) -> Option<()> where C: Cut + SearchNearestParameter, @@ -356,14 +362,14 @@ impl LoopsStore { .absolute_front() .clone(); v.set_point(old_vertex.point()); - self.change_vertex(&old_vertex, v, emap); + self.change_vertex(&old_vertex, v, emap, &mut |old, new| lineage.edge_piece(old, new)); } ParameterKind::Back => { let old_vertex = self[loops_index][wire_index][edge_index] .absolute_back() .clone(); v.set_point(old_vertex.point()); - self.change_vertex(&old_vertex, v, emap); + self.change_vertex(&old_vertex, v, emap, &mut |old, new| lineage.edge_piece(old, new)); } ParameterKind::Inner(_) => { let curve = self[loops_index][wire_index][edge_index].curve(); @@ -373,6 +379,8 @@ impl LoopsStore { let edge = self[loops_index][wire_index][edge_index].absolute_clone(); let edge_id = edge.id(); let (edge0, edge1) = edge.cut_with_parameter(v, t)?; + lineage.edge_piece(edge_id, edge0.id()); + lineage.edge_piece(edge_id, edge1.id()); let new_wire: Wire<_, _> = vec![edge0, edge1].into(); self.swap_edge_into_wire(edge_id, &new_wire); } @@ -445,6 +453,7 @@ pub fn create_loops_stores( geom_shell1: &Shell, poly_shell1: &Shell>, tol: f64, + lineage: &mut Lineage, ) -> Option> where C: SearchNearestParameter @@ -466,6 +475,7 @@ where let ori1 = geom_shell1[face_index1].orientation(); let surface0 = geom_shell0[face_index0].surface(); let surface1 = geom_shell1[face_index1].surface(); + let meeting = [geom_shell0[face_index0].id(), geom_shell1[face_index1].id()]; let polygon0 = crate::tr!(poly_shell0[face_index0].surface(), "polygon0"); let polygon1 = crate::tr!(poly_shell1[face_index1].surface(), "polygon1"); intersection_curve::intersection_curves( @@ -492,6 +502,7 @@ where poly_loops_store1[face_index1] .add_independent_loop(BoundaryWire::new(poly_wire, status1)); let geom_wire = create_independent_loop(intersection_curve); + geom_wire.iter().for_each(|edge| lineage.meet(edge.id(), meeting)); geom_loops_store0[face_index0] .add_independent_loop(BoundaryWire::new(geom_wire.clone(), status0)); geom_loops_store1[face_index1] @@ -514,6 +525,7 @@ where kind, &surface1, &mut gemap0, + lineage, ), "add_geom_vertex 1"); let polyline = intersection_curve.editor().leader; *polyline.first_mut().unwrap() = gv0.point(); @@ -527,6 +539,7 @@ where kind, &surface1, &mut gemap1, + lineage, ), "add_geom_vertex 2"); let polyline = intersection_curve.editor().leader; *polyline.last_mut().unwrap() = gv1.point(); @@ -540,6 +553,7 @@ where kind, &surface0, &mut gemap0, + lineage, ), "add_geom_vertex 3"); let polyline = intersection_curve.editor().leader; *polyline.first_mut().unwrap() = gv0.point(); @@ -553,12 +567,14 @@ where kind, &surface0, &mut gemap1, + lineage, ), "add_geom_vertex 4"); let polyline = intersection_curve.editor().leader; *polyline.last_mut().unwrap() = gv1.point(); } let pedge = Edge::new(&pv0, &pv1, polyline); let gedge = Edge::new(&gv0, &gv1, intersection_curve.into()); + lineage.meet(gedge.id(), meeting); poly_loops_store0[face_index0].add_edge(pedge.clone(), status0); geom_loops_store0[face_index0].add_edge(gedge.clone(), status0); poly_loops_store1[face_index1].add_edge(pedge, status1); diff --git a/vendor/truck-shapeops/src/transversal/loops_store/tests.rs b/vendor/truck-shapeops/src/transversal/loops_store/tests.rs index defbad19..adee82d7 100644 --- a/vendor/truck-shapeops/src/transversal/loops_store/tests.rs +++ b/vendor/truck-shapeops/src/transversal/loops_store/tests.rs @@ -130,7 +130,7 @@ fn independent_intersection() { geom_loops_store0, geom_loops_store1, .. - } = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL).unwrap(); + } = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL, &mut Default::default()).unwrap(); // check the topology let vertex_format = VertexDisplayFormat::AsPoint; @@ -363,7 +363,7 @@ fn rotated_intersection() { geom_loops_store0, geom_loops_store1, .. - } = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL).unwrap(); + } = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL, &mut Default::default()).unwrap(); let vertex_format = VertexDisplayFormat::AsPoint; let edge_id_format = EdgeDisplayFormat::VerticesTupleAndID { vertex_format }; @@ -612,7 +612,7 @@ fn crossing_edges() { geom_loops_store0, geom_loops_store1, .. - } = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL).unwrap(); + } = create_loops_stores(&geom_shell0, &poly_shell0, &geom_shell1, &poly_shell1, TOL, &mut Default::default()).unwrap(); let vertex_format = VertexDisplayFormat::AsPoint; let edge_id_format = EdgeDisplayFormat::VerticesTupleAndID { vertex_format }; diff --git a/vendor/truck-shapeops/src/transversal/mod.rs b/vendor/truck-shapeops/src/transversal/mod.rs index b484ccd8..1d661fed 100644 --- a/vendor/truck-shapeops/src/transversal/mod.rs +++ b/vendor/truck-shapeops/src/transversal/mod.rs @@ -1,5 +1,6 @@ mod divide_face; mod faces_classification; +mod history; mod integrate; mod intersection_curve; // SolveCraft: expose the constructor for workspace regression coverage of the vendored fix. @@ -7,4 +8,5 @@ mod intersection_curve; pub use intersection_curve::IntersectionCurveWithParameters; mod loops_store; mod polyline_construction; -pub use integrate::{and, or, ShapeOpsCurve, ShapeOpsSurface}; +pub use history::{EdgeOrigin, FaceIndex, FaceOrigin, History}; +pub use integrate::{and, and_with_history, or, or_with_history, ShapeOpsCurve, ShapeOpsSurface};