feat(kernel): history across coalesced stepped walls (#136)

A cut of stepped prisms sews a wall over one segment through successive
slabs as one face, and the generated planar walls are then joined on
their common supports. Until now such a piece carried no history.

- prism_lineage: a sewn wall records what it is over in each slab it
  spans (Spec::Wall { levels, slabs }), so a coalesced wall comes from
  the operand walls under it in every slab
- the sewn piece's history is attached before the joining and carried
  through it (Built::Traced); planar_regularize composes the history of
  the body it joins with the join itself
- a composed face is Split only where record::mark_shared_split finds an
  origin under several faces; an inner Split alone no longer makes one
- Body::same ignores history: an operation that leaves a body as it is
  gives the same body, carrying that operation's history

The two boolean history tests that waited for this run again, and the
coalesced and regularized step cuts are covered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 97bcdb4d06
commit 08d797235d
11 files changed
+334 -159

No files matched your search

+3 -6
View File
@@ -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<Body> {
+34 -15
View File
@@ -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<usize> = (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<FaceOrigin>)> { 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
+1 -3
View File
@@ -80,9 +80,7 @@ pub(crate) fn join(pieces: Vec<Traced>, counts: &[(usize, usize)]) -> Result<Opt
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());
}
edges.extend(record::edges_by_identity(p, h));
}
if let Some(faces) = faces.as_mut() {
record::mark_shared_split(faces);
+1 -4
View File
@@ -250,11 +250,10 @@ impl History {
let mut faces = Vec::with_capacity(self.faces.len());
for f in &self.faces {
let mut origins = BTreeSet::new();
let (mut split, mut kept) = (f.change == Change::Split, f.change == Change::Kept);
let mut kept = f.change == Change::Kept;
for o in &f.origins {
let (os, change) = map_face(*o)?;
origins.extend(os);
split |= change == Some(Change::Split);
kept &= change.is_none_or(|c| c == Change::Kept);
}
let origins: Vec<FaceOrigin> = 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 {
+3 -2
View File
@@ -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));
+70 -29
View File
@@ -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();
}
}
}
+25 -1
View File
@@ -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<Vec<usize>>) -> 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> {
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<usize>> = 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))
}
+51 -47
View File
@@ -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<Result<(Body, Vec<lineage::Spec>, Option<Vec<usize>>)>> {
history: &lineage::Context,
) -> Option<Result<(Body, lineage::Built)>> {
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<Section> {
@@ -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<lineage::Slab>);
let mut runs: Vec<Run> = 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<lineage::Spec> = 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(&reg.outer).chain(&reg.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<FaceSpec> = Vec::new();
let mut sources = Vec::new();
let mut specs: Vec<lineage::Spec> = 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 };
+140 -51
View File
@@ -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<Option<Vec<Cap>>>,
}
/// How one piece of the result was built.
@@ -62,17 +64,53 @@ pub(super) enum Built {
/// 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<Spec>, faces: Option<Vec<usize>> },
/// Built some other way, carrying its own history over the operands (none: not known).
Traced,
}
/// 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 wall over one section segment through successive slabs, between successive `levels`
/// (one more than `slabs`): what it is over in each slab.
Wall { levels: Vec<f64>, slabs: Vec<Slab> },
/// A cap over `section` at level `z`, facing along the direction when `up`.
Cap { section: Section, z: f64, up: bool },
}
/// A wall in one slab: over a segment tagged `tag`; `next` is the wall over the next segment
/// of its loop in that slab, which it meets at `at`.
pub(super) struct Slab {
pub tag: Tag,
pub next: usize,
pub at: Vec2,
}
impl Spec {
/// The wall's slab between levels `lo` and `hi` (`hi` above `lo`).
fn slab(&self, lo: f64, hi: f64) -> Option<&Slab> {
let Spec::Wall { levels, slabs } = self else { return None };
let mid = (lo + hi) / 2.0;
slabs.iter().zip(levels.windows(2)).find(|(_, z)| z[0] < mid && mid < z[1]).map(|(s, _)| s)
}
/// The wall's slab at its bottom level, when that is `z`.
fn bottom(&self, z: f64, tol: f64) -> Option<&Slab> {
let Spec::Wall { levels, slabs } = self else { return None };
((levels.first()? - z).abs() <= tol).then_some(slabs.first()?)
}
/// The wall's slab at its top level, when that is `z`.
fn top(&self, z: f64, tol: f64) -> Option<&Slab> {
let Spec::Wall { levels, slabs } = self else { return None };
((levels.last()? - z).abs() <= tol).then_some(slabs.last()?)
}
/// The wall's slab at its bottom or top level `z`.
fn end(&self, z: f64, tol: f64) -> Option<&Slab> {
self.bottom(z, tol).or_else(|| self.top(z, tol))
}
}
/// A face of a piece: the wall over a segment with this tag between two levels, or a cap.
#[derive(Clone, Copy)]
enum Kind {
@@ -80,7 +118,11 @@ enum Kind {
Cap(usize),
}
impl Context<'_> {
impl<'a> Context<'a> {
pub(super) fn new(op: BoolOp, operands: [&'a Body; 2], index: [&'a Index; 2], plane: Plane, tol: f64, heights: Option<[(f64, f64); 2]>) -> Self {
Context { op, operands, index, plane, tol, heights, caps: std::cell::OnceCell::new() }
}
/// `body`, made of `pieces` (their shells in order), with its history over the operands
/// (none where it is not known).
pub(super) fn attach<B: std::borrow::Borrow<Body>>(&self, body: Body, pieces: &[(B, Built)]) -> Body {
@@ -92,18 +134,26 @@ impl Context<'_> {
/// 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<FaceProv>, HashMap<mt::EdgeID, Vec<EdgeOrigin>>)> {
let caps = self.caps()?;
let caps = self.caps.get_or_init(|| self.caps()).as_deref()?;
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()?)?,
Built::Extruded { section, lo, hi } => self.extruded(caps, body, section, *lo, *hi)?,
Built::Sewn { specs, faces } => self.sewn(caps, body, specs, faces.as_deref()?)?,
Built::Traced => {
let h = body.history()?;
faces.extend(h.faces.iter().cloned());
edges.extend(crate::record::edges_by_identity(body, h));
continue;
}
};
faces.extend(f);
faces.extend(f.into_iter().map(|(origins, whole)| crate::record::region(origins, whole)));
edges.extend(e);
}
Some((crate::record::regions(faces), edges))
// Pieces of one body can share an origin.
crate::record::mark_shared_split(&mut faces);
Some((faces, edges))
}
/// Both operands' caps, their outlines tagged.
@@ -157,16 +207,9 @@ impl Context<'_> {
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<usize>) -> Vec<usize> {
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()
/// The operand's edges (by number) bounding exactly the faces `faces`.
fn edges_between(&self, operand: usize, [f, g]: [usize; 2]) -> &[usize] {
self.index.get(operand).and_then(|index| index.between.get(&(f.min(g), f.max(g)))).map_or(&[], Vec::as_slice)
}
/// Is the wall over a segment tagged `t` between `lo` and `hi` its operand wall untouched?
@@ -199,7 +242,7 @@ impl Context<'_> {
if let Some(e) = [t.edge, t.far].into_iter().flatten().find(beside) {
return Some(vec![EdgeOrigin::Edge { operand: x, edge: e }]);
}
let along: Vec<usize> = own.iter().flat_map(|c| self.edges_between(x, [w, *c], None)).collect();
let along: Vec<usize> = own.iter().flat_map(|c| self.edges_between(x, [w, *c])).copied().collect();
match along.as_slice() {
[e] => Some(vec![EdgeOrigin::Edge { operand: x, edge: *e }]),
[] => Some(vec![EdgeOrigin::meet(Self::wall(t)?, Side::new(cap.iter().copied())?)]),
@@ -208,10 +251,11 @@ impl Context<'_> {
}
/// 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, else the one edge between them), where two
/// operands' walls meet, or none within one wall (a segment split there).
fn junction(&self, t1: Tag, t2: Tag) -> Option<Vec<EdgeOrigin>> {
/// `t1`) runs into the second (`t2`), between levels `lo` and `hi`: the operand's edge there
/// between its walls (along the line through the operand vertex the segments meet at, else
/// the one edge between them), where two operands' walls meet, or none within one wall (a
/// segment split there).
fn junction(&self, t1: Tag, t2: Tag, lo: f64, hi: f64) -> Option<Vec<EdgeOrigin>> {
let (p, q) = (Self::wall(t1)?, Self::wall(t2)?);
if t1.operand != t2.operand {
return Some(vec![EdgeOrigin::meet(p, q)]);
@@ -220,12 +264,47 @@ impl Context<'_> {
match (t1.end, t2.start) {
// At that vertex: the operand's edge there (a seam, within one wall), else two edges
// of one wall's cap outline running on through it.
(Some(v), Some(u)) if v == u => self.one_edge(x, [w1, w2], Some(v)).or_else(|| (w1 == w2).then(Vec::new)),
(Some(v), Some(u)) if v == u => self.edge_over(x, [w1, w2], Some(v), lo, hi).or_else(|| (w1 == w2).then(Vec::new)),
_ if w1 == w2 => Some(Vec::new()),
// Elsewhere (a segment split off there, or read off another cap): only the operand's
// walls are around the edge, so the operand has its edge between them there, and
// that is the one edge between them where there is one.
_ => self.one_edge(x, [w1, w2], None),
// that is the one edge between them over these levels where there is one.
_ => self.edge_over(x, [w1, w2], None, lo, hi),
}
}
/// The operand's edge between the walls `faces` that runs over the levels `lo` to `hi`,
/// when it has exactly one; with `at`, among the edges between them chained to that vertex.
/// The segments' vertex was read at one level and carried through others, and the line
/// between two walls is in several edges where vertices an earlier operation left (stations)
/// split it: the levels say which of them a slab's edge runs along.
fn edge_over(&self, operand: usize, faces: [usize; 2], at: Option<usize>, lo: f64, hi: f64) -> Option<Vec<EdgeOrigin>> {
let index = self.index.get(operand)?;
let between = self.edges_between(operand, faces);
if between.len() > 4096 {
return None;
}
let ends: Vec<(Option<usize>, Option<usize>)> =
between.iter().map(|e| index.bounds.get(*e).map_or((None, None), |(_, front, back)| (*front, *back))).collect();
// With `at`: the edges reached from it through shared vertices.
let mut chained = vec![at.is_none(); between.len()];
let mut reached: Vec<usize> = at.into_iter().collect();
while let Some(p) = reached.pop() {
for (k, (front, back)) in ends.iter().enumerate() {
if !chained[k] && (*front == Some(p) || *back == Some(p)) {
chained[k] = true;
reached.extend(if *front == Some(p) { *back } else { *front });
}
}
}
let height = |v: Option<usize>| v.and_then(|v| index.points.get(v)).map(|p| self.plane.height(*p));
let mut over = between.iter().zip(&ends).zip(chained).filter_map(|((e, (front, back)), chained)| {
let (a, b) = (height(*front)?, height(*back)?);
(chained && a.min(b) <= lo + self.tol && a.max(b) >= hi - self.tol).then_some(*e)
});
match (over.next(), over.next()) {
(Some(edge), None) => Some(vec![EdgeOrigin::Edge { operand, edge }]),
_ => None,
}
}
@@ -240,13 +319,13 @@ impl Context<'_> {
if t1.operand != t2.operand {
return Some(vec![EdgeOrigin::meet(p, q)]);
}
self.one_edge(t1.operand, [t1.wall?, t2.wall?], None)
// An edge across the direction at a level: the levels cannot tell edges apart here.
self.one_edge(t1.operand, [t1.wall?, t2.wall?])
}
/// The operand's edge between the faces `faces` (touching vertex `at` when given), when it
/// has exactly one.
fn one_edge(&self, operand: usize, faces: [usize; 2], at: Option<usize>) -> Option<Vec<EdgeOrigin>> {
match self.edges_between(operand, faces, at).as_slice() {
/// The operand's edge between the faces `faces`, when it has exactly one.
fn one_edge(&self, operand: usize, faces: [usize; 2]) -> Option<Vec<EdgeOrigin>> {
match self.edges_between(operand, faces) {
[edge] => Some(vec![EdgeOrigin::Edge { operand, edge: *edge }]),
_ => None,
}
@@ -317,7 +396,7 @@ impl Context<'_> {
(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)?
self.junction(*t1, *t2, lo, hi)?
}
_ => return None,
};
@@ -348,7 +427,7 @@ impl Context<'_> {
let faces = built
.iter()
.map(|i| match specs.get(*i)? {
Spec::Wall { tag, .. } => Some((std::iter::once(Self::wall(*tag)?).collect(), false)),
Spec::Wall { slabs, .. } => Some((slabs.iter().map(|s| Self::wall(s.tag)).collect::<Option<_>>()?, false)),
Spec::Cap { .. } => cap_of.get(*i)?.clone().map(|o| (o, false)),
})
.collect::<Option<Vec<_>>>()?;
@@ -357,24 +436,34 @@ impl Context<'_> {
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)? }
let (x, y) = (specs.get(i)?, specs.get(j)?);
// The edge's levels, lower first, and where it stands in plan.
let (front, back) = (from_p3(e.absolute_front().point()), from_p3(e.absolute_back().point()));
let (h1, h2) = (self.plane.height(front), self.plane.height(back));
let (lo, hi) = (h1.min(h2), h1.max(h2));
let o = match (x, y) {
(Spec::Wall { .. }, Spec::Cap { .. }) => self.cap_edge(x.end(lo, self.tol)?.tag, cap_of.get(j)?.as_ref()?)?,
(Spec::Cap { .. }, Spec::Wall { .. }) => self.cap_edge(y.end(lo, self.tol)?.tag, cap_of.get(i)?.as_ref()?)?,
(Spec::Wall { .. }, Spec::Wall { .. }) if hi - lo <= self.tol => {
// One over the other at a level: the lower first.
let (below, above) = match (x.top(lo, self.tol), y.bottom(lo, self.tol)) {
(Some(s), Some(t)) => (s, t),
_ => (y.top(lo, self.tol)?, x.bottom(lo, self.tol)?),
};
self.stacked(below.tag, above.tag)?
}
(Spec::Wall { .. }, Spec::Wall { .. }) => {
// Side by side within a slab: the vertex the edge stands on (in plan) says
// which way round they meet.
let (s, t) = (x.slab(lo, hi)?, y.slab(lo, hi)?);
let p = self.plane.to_local(front);
let at = |q: &Vec2| p.dist(*q) < self.tol * 10.0;
if s.next == j && at(&s.at) {
self.junction(s.tag, t.tag, lo, hi)?
} else if t.next == i && at(&t.at) {
self.junction(t.tag, s.tag, lo, hi)?
} 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;
}
}
_ => return None,
+5
View File
@@ -136,6 +136,11 @@ pub(crate) fn build(
Ok(traced(Body::new(solid)?, counts, faces, edges))
}
/// The origins of `body`'s edges by identity, from its history `h` (edges in its edge order).
pub(crate) fn edges_by_identity<'a>(body: &'a Body, h: &'a History) -> impl Iterator<Item = (mt::EdgeID, Vec<EdgeOrigin>)> + 'a {
edge_order(body.solid.face_iter()).0.into_iter().zip(&h.edges).map(|(e, p)| (e.id(), p.origins.clone()))
}
/// 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()
+1 -1
View File
@@ -36,8 +36,8 @@ fn surface_of(shell: mt::Shell) -> Body {
paint: None,
splits: None,
face_sources: None,
regularization_protected: false,
history: None,
regularization_protected: false,
}
}