diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs
index f35b5e34..60c98ad0 100644
--- a/crates/kernel/src/body.rs
+++ b/crates/kernel/src/body.rs
@@ -378,7 +378,9 @@ fn polyline_mid(pts: &[Vec3]) -> (Vec3, f64) {
}
impl Body {
- /// The very same body (shared data), not just an equal-looking one.
+ /// The very same body (shared data), not just an equal-looking one. Its history (how an
+ /// operation made it) is not part of it: an operation that leaves a body as it is (a cut by
+ /// a tool clear of it) gives the same body, with that operation's history.
pub fn same(&self, other: &Body) -> bool {
std::sync::Arc::ptr_eq(&self.solid, &other.solid)
&& match (&self.mesh, &other.mesh) {
@@ -391,11 +393,6 @@ impl Body {
&& self.splits == other.splits
&& self.face_sources == other.face_sources
&& self.regularization_protected == other.regularization_protected
- && match (&self.history, &other.history) {
- (Some(a), Some(b)) => std::sync::Arc::ptr_eq(a, b),
- (None, None) => true,
- _ => false,
- }
}
pub(crate) fn new(solid: Solid) -> Result
{
diff --git a/crates/kernel/src/boolean_history_tests.rs b/crates/kernel/src/boolean_history_tests.rs
index c9d4d578..13dd0b39 100644
--- a/crates/kernel/src/boolean_history_tests.rs
+++ b/crates/kernel/src/boolean_history_tests.rs
@@ -290,7 +290,6 @@ fn box_minus_cylinder_prism() {
/// 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);
@@ -368,7 +367,6 @@ fn glue_merges_across_the_seam() {
/// 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);
@@ -425,9 +423,36 @@ fn faceted_tool() {
assert_eq!(h.faces.iter().filter(|f| matches!(f.origins.first(), Some(FaceOrigin::Face { operand: 1, .. }))).count(), 3);
}
-/// A step cut into a certified box: the boolean leaves the faces it split on the box's planes
-/// in pieces, which the planar regularization joins back; the joined body keeps the boolean's
-/// history composed through the join.
+/// A partial-depth cut of stepped prisms: walls over one segment through successive slabs are
+/// sewn as one face (coalesced), which comes from the operand walls under it in every slab.
+#[test]
+fn coalesced_stepped_walls() {
+ use solvecraft_geom::{Loop2, Plane, Region2, Vec2};
+ for curved in [true, false] {
+ let (outer, inner) = if curved {
+ (Loop2::circle(Vec2::ZERO, 10.0), Loop2::circle(Vec2::ZERO, 5.0))
+ } else {
+ let square = |h: f64| Loop2::polygon(&[Vec2::new(-h, -h), Vec2::new(h, -h), Vec2::new(h, h), Vec2::new(-h, h)]);
+ (square(10.0), square(5.0))
+ };
+ let stock = crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0.0, 20.0).unwrap().pop().unwrap();
+ let cutter = crate::extrude(&Plane::XY, &[Region2 { outer: inner, holes: vec![] }], 2.0, 22.0).unwrap().pop().unwrap();
+ let r = boolean(&stock, &cutter, BoolOp::Cut).unwrap().unwrap();
+ let h = verified(&r, &[&stock, &cutter], 0.01);
+ // The stock's walls run whole from 0 to 20, one face each, past the cutter's floor.
+ let walls: Vec = (0..r.face_count()).filter(|i| r.face_was_coalesced(*i)).collect();
+ assert_eq!(walls.len(), if curved { 2 } else { 4 }, "{}", show(h));
+ for i in walls {
+ assert!(matches!(h.faces[i].origins.as_slice(), [FaceOrigin::Face { operand: 0, .. }]), "{}", show(h));
+ assert_eq!(h.faces[i].change, Change::Modified, "{}", show(h));
+ }
+ }
+}
+
+/// A step cut into a box: the stepped prism path sews the box's face 3 in pieces at the
+/// slab stations, and the joining of generated walls on their common supports makes it one face
+/// again; the joined body keeps the sewn body's history composed through the join. A certified
+/// box (whose planar regularization would join the same pieces) gets the same history.
#[test]
fn regularized_step_cut() {
let (pa, pb) = (boxed(0.0, 0.0, 0.0, 30.0, 35.0, 7.5), boxed(0.0, 30.0, 2.5, 22.5, 50.0, 25.0));
@@ -436,16 +461,10 @@ fn regularized_step_cut() {
let h = verified(&r, &[&a, &b], 0.01);
let plain = boolean(&pa, &pb, BoolOp::Cut).unwrap().unwrap();
let ph = verified(&plain, &[&pa, &pb], 0.01);
- // Uncertified, the box's face 3 stays in two pieces; certified, they are joined into one
- // face, the only one from it.
- assert_eq!(from(ph, face(0, 3)).len(), 2, "{}", show(ph));
- assert_eq!(r.face_count() + 1, plain.face_count());
- let joined = from(h, face(0, 3));
- assert_eq!(joined.len(), 1, "{}", show(h));
- assert_eq!(faces_of(h)[joined[0]], (Change::Modified, vec![face(0, 3)]));
- // Every other face is as the boolean made it, in its order.
- let rest = |h: &History| -> Vec<(Change, Vec)> { faces_of(h).into_iter().filter(|(_, o)| *o != [face(0, 3)]).collect() };
- assert_eq!(rest(h), rest(ph));
+ let joined = from(ph, face(0, 3));
+ assert_eq!(joined.len(), 1, "{}", show(ph));
+ assert_eq!(faces_of(ph)[joined[0]], (Change::Modified, vec![face(0, 3)]));
+ assert_eq!(faces_of(h), faces_of(ph), "{}", show(h));
}
/// A ring joined under a floor, its bore on the floor's hole (coincident cylinders): the ring's
diff --git a/crates/kernel/src/boolean_record.rs b/crates/kernel/src/boolean_record.rs
index 272b6f28..2f3060a1 100644
--- a/crates/kernel/src/boolean_record.rs
+++ b/crates/kernel/src/boolean_record.rs
@@ -80,9 +80,7 @@ pub(crate) fn join(pieces: Vec, counts: &[(usize, usize)]) -> Result = origins.into_iter().collect();
@@ -262,8 +261,6 @@ impl History {
Change::Generated
} else if origins.len() > 1 {
Change::Merged
- } else if split {
- Change::Split
} else if kept {
Change::Kept
} else {
diff --git a/crates/kernel/src/history_tests.rs b/crates/kernel/src/history_tests.rs
index ea7ef05b..86471d76 100644
--- a/crates/kernel/src/history_tests.rs
+++ b/crates/kernel/src/history_tests.rs
@@ -128,8 +128,9 @@ fn substitute_composes_kept_split_merged_and_generated_faces() {
assert_eq!(op2.operands, 4);
let f = &op2.faces;
assert_eq!(f[0], prov(vec![face(1, 0)], Change::Kept));
- // Two pieces of one face, merged back: one origin, so not a merge across faces.
- assert_eq!(f[1], prov(vec![face(1, 1)], Change::Split));
+ // Two pieces of one face, merged back: one origin, so not a merge across faces, and no
+ // other face comes from it, so not split.
+ assert_eq!(f[1], prov(vec![face(1, 1)], Change::Modified));
assert_eq!(f[2], prov(vec![face(2, 4)], Change::Modified));
assert_eq!(f[3], prov(vec![FaceOrigin::Profile { segment: 2 }], Change::Generated));
assert_eq!(f[4], prov(vec![face(0, 5), face(3, 1)], Change::Merged));
diff --git a/crates/kernel/src/modify_history_tests.rs b/crates/kernel/src/modify_history_tests.rs
index ca3eb8c1..eab98323 100644
--- a/crates/kernel/src/modify_history_tests.rs
+++ b/crates/kernel/src/modify_history_tests.rs
@@ -248,6 +248,31 @@ fn shell_of_a_body_that_is_not_convex() {
assert_eq!(offsets, 7, "{h:?}");
}
+/// A shell of a block with a tab joined on a side: the cavity Cut sews the walls in pieces
+/// across its slabs and joins them on their common supports, the history carried through.
+#[test]
+fn shell_of_a_stepped_body_joins_its_walls() {
+ let stock = box_solid(Vec3::new(-20.0, -15.0, 0.0), Vec3::new(20.0, 15.0, 24.0)).unwrap();
+ let tab = box_solid(Vec3::new(-12.0, -25.0, 8.0), Vec3::new(-4.0, -15.0, 16.0)).unwrap();
+ let b = boolean(&stock, &tab, BoolOp::Union).unwrap().unwrap();
+ let r = crate::shell(&b, &[Vec3::new(0.0, 0.0, 24.0)], 1.0).unwrap();
+ let h = verified(&r, &b, reach(1.0 * 1.5));
+ // Every face of the body but the open top is lined by one inner wall.
+ let offsets = h.faces.iter().filter(|f| matches!(f.origins.as_slice(), [FaceOrigin::Offset { .. }])).count();
+ assert_eq!(offsets, b.face_count() - 1, "{h:?}");
+}
+
+/// A shell of a painted block: its faces are used as they are (unhealed), keeping the paint's
+/// face order, with the history of that copy. Paint is an authored boundary, so the cavity Cut
+/// keeps the side faces it sews across the rim's level in pieces (Split).
+#[test]
+fn shell_of_a_painted_body() {
+ let b = painted_cube();
+ let r = crate::shell(&b, &[Vec3::new(20.0, 15.0, 20.0)], 2.0).unwrap();
+ let h = verified(&r, &b, reach(2.0));
+ assert_eq!(changes(h), [0, 2, 8, 0, 5], "{h:?}");
+}
+
#[test]
fn draft_tilts_faces() {
let b = cube();
@@ -377,10 +402,15 @@ fn mirrored_copy_keeps_every_face_and_edge() {
}
}
-/// The block with box cuts of each common shape: a notch through the back, a pocket, a step
-/// along one end, a slot across the top, a hole through, a corner bitten out.
-fn box_cuts() -> Vec<(&'static str, Body, Body)> {
- let b = cube();
+/// The block with a face painted: an authored boundary, so the cuts leave the faces they split
+/// across their slabs in pieces (none of them joined on their common supports).
+fn painted_cube() -> Body {
+ cube().with_paint(Some(crate::Paint { opacity: 1., faces: vec![crate::FacePaint { face: 0, color: [1., 0., 0.], opacity: 1. }] }))
+}
+
+/// The block `b` with box cuts of each common shape: a notch through the back, a pocket, a
+/// step along one end, a slot across the top, a hole through, a corner bitten out.
+fn box_cuts_of(b: &Body) -> Vec<(&'static str, Body, Body)> {
[
("notch", Vec3::new(15.0, 25.0, -5.0), Vec3::new(25.0, 35.0, 25.0)),
("pocket", Vec3::new(10.0, 10.0, 10.0), Vec3::new(30.0, 20.0, 25.0)),
@@ -392,7 +422,7 @@ fn box_cuts() -> Vec<(&'static str, Body, Body)> {
.into_iter()
.map(|(name, lo, hi)| {
let tool = box_solid(lo, hi).unwrap();
- let cut = boolean(&b, &tool, BoolOp::Cut).unwrap().unwrap();
+ let cut = boolean(b, &tool, BoolOp::Cut).unwrap().unwrap();
(name, cut, tool)
})
.collect()
@@ -403,41 +433,52 @@ fn merged_meetings(h: &History) -> usize {
h.edges.iter().flat_map(|e| &e.origins).filter(|o| matches!(o, EdgeOrigin::Meet { a, b } if a.single().is_none() || b.single().is_none())).count()
}
-/// A fillet right after a box cut. Where the cut leaves coplanar pieces (the step, slot and
-/// bite leave the front face in pieces), the fillet's heal merges them, and the edge where the
-/// round runs into that face meets all of it. Composed with the cut, over the block and the tool.
+/// A fillet right after a box cut. The cut joins the walls it sews in pieces across its slabs
+/// on their common supports, so the fillet finds whole faces. Where the block is painted the cut
+/// keeps those pieces (it has more faces), the fillet's heal merges them, and the edges where
+/// the round runs into such a face meet all of it. Composed with the cut, over the block and
+/// the tool.
#[test]
fn fillet_after_box_cuts() {
- let b = cube();
let at = Vec3::new(5.0, 0.0, 20.0);
- for (name, cut, tool) in box_cuts() {
- let r = crate::fillet(&cut, &[at], 2.0).unwrap();
- let h = verified(&r, &cut, reach(2.0 * 1.5));
- assert_eq!(blends(h), [vec![edge(edge_at(&cut, at))]], "{name}");
- let merged = matches!(name, "step" | "slot" | "bite");
- assert_eq!(merged_meetings(h) > 0, merged, "{name}: {h:?}");
- let mut through = h.clone();
- through.substitute(0, cut.history().unwrap()).unwrap();
- through.check(&r, &[&b, &tool]).unwrap();
+ let plain = box_cuts_of(&cube());
+ for (b, painted) in [(cube(), false), (painted_cube(), true)] {
+ let cuts = if painted { box_cuts_of(&b) } else { plain.clone() };
+ for ((name, cut, tool), (_, whole, _)) in cuts.into_iter().zip(&plain) {
+ let r = crate::fillet(&cut, &[at], 2.0).unwrap();
+ let h = verified(&r, &cut, reach(2.0 * 1.5));
+ assert_eq!(blends(h), [vec![edge(edge_at(&cut, at))]], "{name}");
+ let pieces = cut.face_count() > whole.face_count();
+ assert_eq!(pieces, painted && name != "notch" && name != "hole", "{name} (painted: {painted})");
+ assert_eq!(merged_meetings(h) > 0, pieces, "{name} (painted: {painted}): {h:?}");
+ let mut through = h.clone();
+ through.substitute(0, cut.history().unwrap()).unwrap();
+ through.check(&r, &[&b, &tool]).unwrap();
+ }
}
}
/// A chamfer right after a box cut, its cutter's cut going the prism way along the edge: the
/// cutter's walls meet the cut body's walls where a segment was split off. The cut body's ends
/// have their outlines in collinear edges (pocket, step, bite), each matched by the segment
-/// running along it; walls the cut left in pieces across the slabs (step, slot, bite) are
-/// followed through the levels where one piece gives way to the next. Composed with the cut,
-/// over the block and the tool.
+/// running along it; walls the cut left in pieces across the slabs (step, slot, bite of the
+/// painted block) are followed through the levels where one piece gives way to the next. Where
+/// the cut joined those pieces, the line between two walls stays in edges split at the levels,
+/// and each slab's edge runs along the one over its levels. Composed with the cut, over the
+/// block and the tool.
#[test]
fn chamfer_after_box_cuts() {
- let b = cube();
let at = Vec3::new(5.0, 0.0, 20.0);
- for (name, cut, tool) in box_cuts() {
- let r = crate::chamfer(&cut, &[at], 2.0).unwrap();
- let h = verified(&r, &cut, reach(2.0 * 1.5));
- assert_eq!(with(h, &[edge(edge_at(&cut, at))]).len(), 1, "{name}: {h:?}");
- let mut through = h.clone();
- through.substitute(0, cut.history().unwrap()).unwrap();
- through.check(&r, &[&b, &tool]).unwrap();
+ for (b, painted) in [(cube(), false), (painted_cube(), true)] {
+ for (name, cut, tool) in box_cuts_of(&b) {
+ let r = crate::chamfer(&cut, &[at], 2.0).unwrap();
+ let h = verified(&r, &cut, reach(2.0 * 1.5));
+ // On the painted block the chamfer face keeps the boundaries the cut left.
+ let faces = with(h, &[edge(edge_at(&cut, at))]).len();
+ assert!(if painted { faces >= 1 } else { faces == 1 }, "{name} (painted: {painted}): {h:?}");
+ let mut through = h.clone();
+ through.substitute(0, cut.history().unwrap()).unwrap();
+ through.check(&r, &[&b, &tool]).unwrap();
+ }
}
}
diff --git a/crates/kernel/src/planar_regularize.rs b/crates/kernel/src/planar_regularize.rs
index 7dda1687..79593d2b 100644
--- a/crates/kernel/src/planar_regularize.rs
+++ b/crates/kernel/src/planar_regularize.rs
@@ -1,6 +1,7 @@
//! Exact topological removal of construction-induced planar subdivisions.
//! Shared edge identity and complete construction lineage select the groups.
use super::{Body, from_p3};
+use crate::history::FaceOrigin;
use crate::{Result, guard};
use solvecraft_geom::Plane;
use std::collections::{BTreeMap, HashMap, HashSet};
@@ -115,6 +116,24 @@ fn merged_face(faces: &[&mt::Face], members: &[usize], plane: Plane, eligible: &
mt::Face::try_new(wires, faces.get(*members.first()?)?.oriented_surface()).ok()
}
+/// `body` with its history: `input`'s faces with each output face made of the input faces
+/// `members` lists for it (every edge of `body` one of `input`'s, by identity), composed with
+/// the history of the operation that made `input`; none where `input` has none.
+fn with_joined_history(body: Body, input: &Body, members: Vec>) -> Body {
+ let Some(inner) = input.history() else { return body };
+ let faces = members.into_iter().map(|m| (m.iter().map(|&face| FaceOrigin::Face { operand: 0, face }).collect(), m.len() == 1)).collect();
+ let joined = crate::record::traced(
+ body,
+ &crate::record::counts(&[input]),
+ Some(crate::record::regions(faces)),
+ Some(&crate::record::edge_origins(&input.solid, 0)),
+ );
+ // Both steps are checked (`traced` against `input`, `through` composing over its checked
+ // history); the composed operands' counts are not known here, so it is set in place.
+ let history = crate::boolean_record::through(&joined, 0, Some(inner));
+ Body { history: history.map(Arc::new), ..joined }
+}
+
impl Body {
pub(crate) fn regularize_certified_planar_faces(self) -> Result {
self.regularize_planar_faces(false)
@@ -252,6 +271,8 @@ impl Body {
let mut shells = Vec::new();
let mut output_sources = Vec::new();
let mut output_ids = Vec::new();
+ // The input faces each output face is made of, for its history.
+ let mut output_members: Vec> = Vec::new();
let mut offset = 0;
for shell in self.solid.boundaries() {
let mut output = Vec::new();
@@ -260,12 +281,15 @@ impl Body {
if let Some(merged) = replacements.get(&leader) {
if offset == leader {
output.push(merged.clone());
+ let Some(members) = groups.remove(&leader) else { return Ok(None) };
+ output_members.push(members);
} else {
offset += 1;
continue;
}
} else {
output.push(face.clone());
+ output_members.push(vec![offset]);
}
output_ids.push(output.last().ok_or_else(|| crate::KernelError::Failed("missing regularized face".into()))?.id());
let source = sources[offset].as_ref();
@@ -291,7 +315,7 @@ impl Body {
if !body.solid.face_iter().map(|f| f.id()).eq(output_ids) {
return Ok(None);
}
- Ok(Some(body.with_face_sources(Some(Arc::new(output_sources)))))
+ Ok(Some(with_joined_history(body.with_face_sources(Some(Arc::new(output_sources))), &self, output_members)))
})?;
Ok(result.unwrap_or(self))
}
diff --git a/crates/kernel/src/prism.rs b/crates/kernel/src/prism.rs
index dbbfa298..9711cf0d 100644
--- a/crates/kernel/src/prism.rs
+++ b/crates/kernel/src/prism.rs
@@ -129,8 +129,10 @@ struct Prism {
/// (`wall`), and the operand's vertices (by [`Index`]) the segment starts and ends at (`None`
/// where it was split off a longer one). Where the wall ends with a slab of a stepped operand, at
/// a cap whose outline runs along the whole segment, `far` is the operand's edge there (the
-/// segment may have been read off a cap at another level). `None` fields: not known (no history
-/// then).
+/// segment may have been read off a cap at another level). A segment carried through levels
+/// beside the same wall keeps the vertices it was read at, which may lie at another level: they
+/// say which line two walls meet along, not which edge of it. `None` fields: not known (no
+/// history then).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Tag {
pub operand: usize,
@@ -1094,7 +1096,8 @@ fn sew_stack(plane: &Plane, d: Vec3, pa: &Prism, pb: &Prism, tol: f64, history:
sew_sections(plane, d, &levels, &slabs, tol, false, history)
}
-/// Sew the exact sections between successive levels, including empty gaps.
+/// Sew the exact sections between successive levels, including empty gaps. With `coalesce`,
+/// walls over one segment through successive slabs are one face.
fn sew_sections(
plane: &Plane,
d: Vec3,
@@ -1154,11 +1157,11 @@ fn sew_sections(
pieces.push((piece, lineage::Built::Extruded { section: tagged.region(r), lo: levels[first], hi: levels[end] }));
}
} else {
- let (piece, specs, faces) = sew_connected_sections(plane, d, &levels[first..=end], &slabs[first..end], tol, coalesce)
+ let (piece, built) = sew_connected_sections(plane, d, &levels[first..=end], &slabs[first..end], tol, coalesce, history)
.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, lineage::Built::Sewn { specs, faces }));
+ pieces.push((piece, built));
}
}
let mut old = std::collections::HashMap::new();
@@ -1199,7 +1202,6 @@ fn wall_key(s: &Seg2) -> Option<[u64; 6]> {
}
/// A genuinely stepped connected axial block has no single extrusion footprint.
-#[allow(clippy::type_complexity)]
fn sew_connected_sections(
plane: &Plane,
d: Vec3,
@@ -1207,7 +1209,8 @@ fn sew_connected_sections(
slabs: &[Section],
tol: f64,
coalesce: bool,
-) -> Option, Option>)>> {
+ history: &lineage::Context,
+) -> 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: &Section, y: &Section| -> Option {
@@ -1244,52 +1247,54 @@ fn sew_connected_sections(
};
// Exact oriented generated segments alone define a run. No support fitting,
// proximity matching, or old face lineage is used here.
- let mut runs: Vec<(usize, usize, Seg2)> = Vec::new();
+ // Each run, with what its wall is over in each of its slabs (for the result's history).
+ type Run = (usize, usize, Seg2, Vec);
+ let mut runs: 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() {
- 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();
+ // The loop's walls in order: the run each joins, its tag and where it ends.
+ let mut around: Vec<(usize, Tag, Vec2)> = Vec::new();
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;
- }
+ let prior = if coalesce { last.get(&key).copied() } else { None };
+ let run = match prior.map(|index| runs.get_mut(index).map(|run: &mut Run| (index, run))) {
+ Some(None) => return None,
+ Some(Some((index, run))) if run.1 == i => {
+ run.1 = i + 1;
+ index
}
- last.insert(key, runs.len());
- } else {
- specs.push(lineage::Spec::Wall { tag: tag.piece(k, n), z0, next: specs.len() + 1, at: sg.end() });
- }
- runs.push((i, i + 1, sg));
+ // Duplicate oriented boundaries in one slab are not
+ // a valid reason to reconstruct a larger wall.
+ Some(Some((_, run))) if run.1 == i + 1 => return None,
+ _ => {
+ if coalesce {
+ last.insert(key, runs.len());
+ }
+ runs.push((i, i + 1, sg, Vec::new()));
+ runs.len() - 1
+ }
+ };
+ around.push((run, tag.piece(k, n), sg.end()));
}
}
- // The loop's last wall runs into its first.
- if !coalesce && let Some(lineage::Spec::Wall { next, .. }) = specs.last_mut() {
- *next = first;
+ // Each wall runs into the next one of its loop, the last into the first.
+ for (k, (run, tag, at)) in around.iter().enumerate() {
+ let next = around.get((k + 1) % around.len())?.0;
+ runs.get_mut(*run)?.3.push(lineage::Slab { tag: *tag, next, at: *at });
}
}
}
}
let mut faces: Vec = Vec::new();
let mut sources = Vec::new();
+ let mut specs: Vec = Vec::with_capacity(runs.len());
let frame_is_orthonormal = plane.origin.is_finite()
&& plane.x.is_finite()
&& plane.y.is_finite()
@@ -1300,7 +1305,7 @@ fn sew_connected_sections(
&& plane.x.dot(plane.y).abs() <= 64. * f64::EPSILON
&& plane.x.dot(d).abs() <= 64. * f64::EPSILON
&& plane.y.dot(d).abs() <= 64. * f64::EPSILON;
- for (first, end, sg) in runs {
+ for (first, end, sg, over) in runs {
let (z0, z1) = (levels[first], levels[end]);
let (a, b) = (sg.start(), sg.end());
let mut lp3 = Vec::with_capacity(2 + 2 * (end - first));
@@ -1342,6 +1347,7 @@ fn sew_connected_sections(
plane: generated_plane,
coalesced: end > first + 1,
}));
+ specs.push(lineage::Spec::Wall { levels: levels.get(first..=end)?.to_vec(), slabs: over });
}
for (z, up, rs) in &caps {
for (ri, r) in rs.regions.iter().enumerate() {
@@ -1359,11 +1365,16 @@ fn sew_connected_sections(
specs.push(lineage::Spec::Cap { section: rs.region(ri), z: *z, up: *up });
}
}
- let sewn = crate::sew::sew_touching_with_sources(&faces, &sources);
+ let sewn = crate::sew::sew_touching_with_sources(&faces, &sources).map(|(body, from)| (body, lineage::Built::Sewn { specs, faces: from }));
+ // Generated walls joined on their common supports: the piece's history over the operands,
+ // carried through the joining.
Some(if coalesce {
- sewn.and_then(|(body, _)| Ok((body.regularize_generated_planar_faces()?, specs, None)))
+ sewn.and_then(|(body, built)| {
+ let body = history.attach(body.clone(), &[(&body, built)]).regularize_generated_planar_faces()?;
+ Ok((body, lineage::Built::Traced))
+ })
} else {
- sewn.map(|(body, from)| (body, specs, from))
+ sewn
})
}
@@ -1399,7 +1410,7 @@ fn simple_prism_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2
let pb = prism_tagged(b, d, &plane, tol, ib)?;
Some((d, plane, pa, pb))
})?;
- let history = lineage::Context { op, operands: [a, b], index: [ia.get(), ib.get()], plane, tol, heights: Some([(pa.lo, pa.hi), (pb.lo, pb.hi)]) };
+ let history = lineage::Context::new(op, [a, b], [ia.get(), ib.get()], plane, tol, Some([(pa.lo, pa.hi), (pb.lo, pb.hi)]));
let (blo, bhi) = heights_of(b, &plane);
let spans = blo <= pa.lo + tol && bhi >= pa.hi - tol;
let same = (pb.lo - pa.lo).abs() < tol && (pb.hi - pa.hi).abs() < tol;
@@ -1613,7 +1624,7 @@ fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2]) -> O
if slabs.iter().all(|s| s.regions.is_empty()) {
return Some(Ok(None));
}
- let history = lineage::Context { op, operands: [a, b], index: [ia.get(), ib.get()], plane, tol, heights: None };
+ let history = lineage::Context::new(op, [a, b], [ia.get(), ib.get()], plane, tol, None);
let coalesce = op == BoolOp::Cut
&& !a.regularization_protected
&& !b.regularization_protected
@@ -1801,14 +1812,7 @@ mod planar_tests {
// 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 history = lineage::Context::new(BoolOp::Cut, [&operand, &operand], [index.get(), index.get()], Plane::XY, 1e-7, 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 };
diff --git a/crates/kernel/src/prism_lineage.rs b/crates/kernel/src/prism_lineage.rs
index d3f8c957..fdca95a4 100644
--- a/crates/kernel/src/prism_lineage.rs
+++ b/crates/kernel/src/prism_lineage.rs
@@ -53,6 +53,8 @@ pub(super) struct Context<'a> {
pub plane: Plane,
pub tol: f64,
pub heights: Option<[(f64, f64); 2]>,
+ /// Both operands' caps, read once (see [`Context::caps`]).
+ caps: std::cell::OnceCell