feat(shapeops): face and edge history from booleans

and_with_history / or_with_history return, with the solid, the operand face
each result face is a region of (and whether it is that face untouched) and,
for each result edge, the operand edge it is (a piece of) or the pair of
operand faces whose intersection made it. Faces and edges are recorded where
they are created (face division, intersection edges, split edges, edges
rebuilt on a moved vertex), resolved to operand terms at creation, so the
passes folding in extra shells compose without bookkeeping. and / or wrap
the same pass and drop the history; a missing record never fails them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
Iraklis KourtisandClaude Opus 5.5 authored and bherbruck committed 2026-10-12 11:52:53 -04:00
1 parent c87efa1f35
commit cc4955a8da
12 files changed
+363 -47

No files matched your search

+1 -1
View File
@@ -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
+8
View File
@@ -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)
+4 -1
View File
@@ -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;
+6 -2
View File
@@ -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<C, S>(
shell: &Shell<Point3, C, S>,
loops_store: &LoopsStore<Point3, C>,
tol: f64,
lineage: &mut Lineage<C, S>,
) -> Option<FacesClassification<Point3, C, S>>
where
C: BoundedCurve<Point = Point3> + ParameterDivision1D<Point = Point3>,
@@ -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(())
})?;
+3 -3
View File
@@ -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);
@@ -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();
+133
View File
@@ -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<FaceOrigin>,
/// Origin of each result edge.
pub edges: Vec<EdgeOrigin>,
}
/// The ids of `solid`'s edges in its edge order (see [`History`]).
fn edge_order<P, C, S>(solid: &Solid<P, C, S>) -> Vec<EdgeID<C>> {
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<K: std::hash::Hash + Eq, V>(map: &mut HashMap<K, V>, key: K, origin: Option<V>) {
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<C, S> {
faces: HashMap<FaceID<S>, FaceOrigin>,
edges: HashMap<EdgeID<C>, EdgeOrigin>,
}
impl<C, S> Default for Lineage<C, S> {
fn default() -> Self { Self { faces: HashMap::default(), edges: HashMap::default() } }
}
impl<C, S> Lineage<C, S> {
/// Every face and edge of the operands, as itself.
pub fn new<P>(operands: [&Solid<P, C, S>; 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<S>, piece: FaceID<S>) {
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<C>, piece: EdgeID<C>) {
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<C>, faces: [FaceID<S>; 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<P>(&self, solid: &Solid<P, C, S>) -> Option<History> {
let faces = solid.face_iter().map(|f| self.faces.get(&f.id()).copied()).collect::<Option<_>>();
let edges = edge_order(solid).into_iter().map(|id| self.edges.get(&id).copied()).collect::<Option<_>>();
Some(History { faces: crate::tr!(faces, "face history"), edges: crate::tr!(edges, "edge history") })
}
}
+49 -28
View File
@@ -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<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
shell0: &Shell<Point3, C, S>,
shell1: &Shell<Point3, C, S>,
tol: f64,
lineage: &mut Lineage<C, S>,
) -> Option<[Shell<Point3, C, S>; 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<C: ShapeOpsCurve<S>, 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<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<Solid<Point3, C, S>> {
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<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<Solid<Point3, C, S>> {
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<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<(Solid<Point3, C, S>, 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<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<(Solid<Point3, C, S>, 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<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
pick: fn([Shell<Point3, C, S>; 2]) -> Shell<Point3, C, S>,
) -> Option<(Solid<Point3, C, S>, Option<History>)> {
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
+129
View File
@@ -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<Edge> {
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::<Vec<_>>());
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);
}
+21 -5
View File
@@ -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<P: Copy, C: Clone> Loops<P, C> {
})
}
// SolveCraft: `replaced(old, new)` is told of every edge rebuilt on the new vertex.
fn change_vertex(
&mut self,
old_vertex: &Vertex<P>,
new_vertex: &Vertex<P>,
emap: &mut HashMap<EdgeID<C>, Edge<P, C>>,
replaced: &mut impl FnMut(EdgeID<C>, EdgeID<C>),
) {
self.iter_mut()
.flat_map(|wire| wire.iter_mut())
@@ -193,6 +196,7 @@ impl<P: Copy, C: Clone> Loops<P, C> {
return;
}
.clone();
replaced(edge.id(), new_edge.id());
if !edge.orientation() {
new_edge.invert();
}
@@ -290,9 +294,10 @@ impl<P: Copy + Tolerance, C: Clone> LoopsStore<P, C> {
old_vertex: &Vertex<P>,
new_vertex: &Vertex<P>,
emap: &mut HashMap<EdgeID<C>, Edge<P, C>>,
replaced: &mut impl FnMut(EdgeID<C>, EdgeID<C>),
) {
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<P: Copy + Tolerance, C: Clone> LoopsStore<P, C> {
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<C> LoopsStore<Point3, C> {
kind: ParameterKind,
another_surface: &S,
emap: &mut HashMap<EdgeID<C>, Edge<Point3, C>>,
lineage: &mut Lineage<C, S>,
) -> Option<()>
where
C: Cut<Point = Point3, Vector = Vector3> + SearchNearestParameter<D1, Point = Point3>,
@@ -356,14 +362,14 @@ impl<C> LoopsStore<Point3, C> {
.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<C> LoopsStore<Point3, C> {
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<C, S>(
geom_shell1: &Shell<Point3, C, S>,
poly_shell1: &Shell<Point3, PolylineCurve, Option<PolygonMesh>>,
tol: f64,
lineage: &mut Lineage<C, S>,
) -> Option<LoopsStoreQuadruple<C>>
where
C: SearchNearestParameter<D1, Point = Point3>
@@ -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);
+3 -3
View File
@@ -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 };
+3 -1
View File
@@ -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};