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>>, } /// 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, faces: Option> }, + /// 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, slabs: Vec }, /// 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>(&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, HashMap>)> { - 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) -> Vec { - let Some(index) = self.index.get(operand) else { return Vec::new() }; - let Some(between) = index.between.get(&(f.min(g), f.max(g))) else { return Vec::new() }; - between - .iter() - .copied() - .filter(|e| at.is_none_or(|v| index.bounds.get(*e).is_some_and(|(_, front, back)| *front == Some(v) || *back == Some(v)))) - .collect() + /// 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 = own.iter().flat_map(|c| self.edges_between(x, [w, *c], None)).collect(); + let along: Vec = 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> { + /// `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> { 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, lo: f64, hi: f64) -> Option> { + let index = self.index.get(operand)?; + let between = self.edges_between(operand, faces); + if between.len() > 4096 { + return None; + } + let ends: Vec<(Option, Option)> = + 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 = 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| 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) -> Option> { - 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> { + 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::>()?, false)), Spec::Cap { .. } => cap_of.get(*i)?.clone().map(|o| (o, false)), }) .collect::>>()?; @@ -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, diff --git a/crates/kernel/src/record.rs b/crates/kernel/src/record.rs index 95e6090b..dcb8cdef 100644 --- a/crates/kernel/src/record.rs +++ b/crates/kernel/src/record.rs @@ -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)> + '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() diff --git a/crates/kernel/src/surfaces.rs b/crates/kernel/src/surfaces.rs index e5e4b605..3101dccc 100644 --- a/crates/kernel/src/surfaces.rs +++ b/crates/kernel/src/surfaces.rs @@ -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, } }