diff --git a/crates/kernel/src/boolean_history_tests.rs b/crates/kernel/src/boolean_history_tests.rs index 9a939f8858..d8157d13b7 100644 --- a/crates/kernel/src/boolean_history_tests.rs +++ b/crates/kernel/src/boolean_history_tests.rs @@ -187,11 +187,11 @@ fn faces_of(h: &History) -> Vec<(Change, Vec)> { h.faces.iter().map(|f| (f.change, f.origins.clone())).collect() } -/// Truck's boolean with a sphere (no prism, no coincident faces): a box with a corner taken +/// A boolean with a sphere (no prism, no coincident faces): a box with a corner taken /// off by a sphere. The three faces at the corner get a notch, the sphere's patches inside the /// box bound it, meeting the box's faces along new edges. #[test] -fn box_minus_sphere_through_truck() { +fn box_minus_sphere() { let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); let b = crate::sphere(Vec3::new(10.0, 10.0, 10.0), 4.0).unwrap(); let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap(); @@ -212,30 +212,6 @@ fn box_minus_sphere_through_truck() { assert!(meets(h).iter().all(|(x, y)| matches!((x, y), (FaceOrigin::Face { operand: 0, .. }, FaceOrigin::Face { operand: 1, .. })))); } -/// The jitter retries move both operands and the result back: the history holds for the -/// operands themselves, the same faces coming from the same operand faces. -#[test] -fn shifted_truck_attempt_keeps_the_history() { - let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); - let b = crate::sphere(Vec3::new(10.0, 10.0, 10.0), 4.0).unwrap(); - let counts = crate::record::counts(&[&a, &b]); - let mut compared = 0; - for op in [BoolOp::Cut, BoolOp::Union, BoolOp::Intersect] { - let Ok((_, Some((plain, _)))) = crate::ops::truck_boolean(&a, &b, op, None, 0.035) else { continue }; - // The shifts the retries use (a hundredth of the size along each). - for j in [[0.1373, 0.2719, 0.3911], [-0.5127, 0.0833, 0.2291], [0.6911, -0.3337, -0.6127]] { - let shift = truck_modeling::Vector3::new(j[0], j[1], j[2]) * 0.17; - let Ok((s, h)) = crate::ops::truck_boolean(&a, &b, op, Some(shift), 0.035) else { continue }; - let faces = h.as_ref().map(|(f, _)| f.clone()); - assert_eq!(faces, Some(plain.clone()), "{op:?}"); - let r = crate::boolean_record::truck_result(s, h, 17.4, &[], &counts).unwrap(); - verified(&r, &[&a, &b], 0.02); - compared += 1; - } - } - assert!(compared > 0); -} - /// Two prisms along x, the tool's section inside the target's, overlapping in part: sewn slab by /// slab, then healed. The target's face at x = 10 gets the tool's outline as a hole, which the /// tool's sides meet; the tool's sides and far end stand out of it. @@ -399,17 +375,6 @@ fn planar_fallback() { } } -/// The nudged retry: a boolean with the tool moved a hair, composed with the move. -#[test] -fn nudged_tool_composes() { - let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); - let b = crate::sphere(Vec3::new(10.0, 10.0, 10.0), 4.0).unwrap(); - let nb = crate::transform(&b, Vec3::new(1e-5, 0.0, 0.0), Vec3::ZERO, Vec3::Z, 0.0).unwrap(); - let r = boolean(&a, &nb, BoolOp::Cut).unwrap().unwrap(); - let r = crate::ops::nudged(r, &nb, &crate::record::counts(&[&a, &b])); - verified(&r, &[&a, &b], 0.02); -} - /// A boolean with a triangle mesh: the mesh's faces are its triangles (and it has no edges). #[test] fn faceted_tool() { @@ -593,7 +558,7 @@ mod smlib { let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0); for (name, b) in [("edge", boxed(10.0, 10.0, 0.0, 20.0, 20.0, 10.0)), ("vertex", boxed(10.0, 10.0, 10.0, 20.0, 20.0, 20.0))] { let r = through_smlib(&a, &b, BoolOp::Union); - assert_eq!(r.solid.boundaries().len(), 2, "{name}"); + assert_eq!(r.shell_count(), 2, "{name}"); assert_eq!((r.face_count(), r.edge_count()), (12, 24), "{name}"); let v = crate::measure(&r).unwrap().volume; assert!((v - 2000.0).abs() < 1e-6 * 2000.0, "{name}: {v}"); @@ -609,7 +574,7 @@ mod smlib { let a = boxed(0.0, 0.0, 0.0, 20.0, 20.0, 10.0); let once = through_smlib(&a, &boxed(10.0, -1.0, -1.0, 21.0, 10.0, 11.0), BoolOp::Cut); let r = through_smlib(&once, &boxed(-1.0, 10.0, -1.0, 10.0, 21.0, 11.0), BoolOp::Cut); - assert_eq!(r.solid.boundaries().len(), 2); + assert_eq!(r.shell_count(), 2); let v = crate::measure(&r).unwrap().volume; assert!((v - 2000.0).abs() < 1e-6 * 2000.0, "{v}"); } @@ -625,7 +590,7 @@ mod smlib { for (name, z0, depth) in [("through", -1.0, 12.0), ("blind", 2.0, 9.0)] { let b = crate::cylinder(Vec3::new(10.0, 15.0, z0), Vec3::Z, 5.0, depth).unwrap(); let r = through_smlib(&a, &b, BoolOp::Cut); - assert_eq!(r.solid.boundaries().len(), 1, "{name}"); + assert_eq!(r.shell_count(), 1, "{name}"); let record = crate::brep_record(&r).unwrap(); assert!(record.edges.iter().all(|e| e.uses.len() == 2), "{name}"); let hole = std::f64::consts::PI * 25.0 * (10.0 - z0.max(0.0)); diff --git a/crates/kernel/src/boolean_record.rs b/crates/kernel/src/boolean_record.rs index 491780bacb..bceaca9a40 100644 --- a/crates/kernel/src/boolean_record.rs +++ b/crates/kernel/src/boolean_record.rs @@ -1,20 +1,13 @@ -//! Recording the exact history of a boolean's result, whichever path made it (see -//! [`crate::ops::boolean`]): for each result face, the operand faces it is a region of; for each -//! result edge, the operand edges it lies along or the operand faces whose meeting it is. The -//! operands are `[a, b]`: the target, then the tool. Paths compose the histories of the steps -//! they take ([`History::over`], [`History::substitute`]); a path that cannot leaves the result -//! without history. - -use std::collections::HashMap; - -use solidcraft_geom::Vec3; -use truck_modeling as mt; +//! Composing the exact history of a multi-step operation (a boolean through several steps, a +//! local modifier on a healed body): each step's history over its own operands is placed among +//! the operation's operands ([`placed`], [`kept`]) and substituted into the next +//! ([`through`], [`of_pair`], [`crate::record::after`]). The operands of a boolean are `[a, b]`: +//! the target, then the tool. A step that cannot report its history leaves the result without +//! one. use crate::Result; -use crate::body::{Body, Solid, edge_order}; -use crate::history::BodyHistory; -use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv, History}; -use crate::record; +use crate::body::Body; +use crate::history::{BodyHistory, History}; /// A composition step's history, or none (logged) where the lineage can't be expressed. pub(crate) fn composed(h: std::result::Result) -> Option { @@ -44,19 +37,6 @@ pub(crate) fn operand(body: &Body, operand: usize, counts: &[(usize, usize)]) -> body.clone().with_checked_history(kept(body, operand, counts.len()), counts) } -/// The histories of `parts`, each made of shells of `whole` (its lumps), over `[whole]`: their -/// faces and edges are the whole's own (by identity). -pub(crate) fn parts_of(parts: &[Body], whole: &Body) -> Vec> { - let (faces, edges) = (record::face_index(&whole.solid), record::edge_origins(&whole.solid, 0)); - parts - .iter() - .map(|part| { - let faces = part.solid.face_iter().map(|f| Some(record::from_face(0, *faces.get(&f.id())?, Change::Kept))).collect::>>()?; - Some(History { operands: 1, faces, edges: record::edge_provs(part, &edges)? }) - }) - .collect() -} - /// A body with its history over the operation's operands (`None`: not known). pub(crate) type Traced = (Body, Option); @@ -66,169 +46,14 @@ pub(crate) fn of_pair(result: &Body, x: &Option, y: &Option) - composed(result.history()?.clone().over(&[x.as_ref()?, y.as_ref()?])) } -/// Bodies apart from each other, each with its history over the operation's operands (with -/// `counts` faces and edges), as one body of several pieces whose history is theirs side by side. -pub(crate) fn join(pieces: Vec, counts: &[(usize, usize)]) -> Result> { - if pieces.len() < 2 { - return Ok(pieces.into_iter().next().map(|(b, h)| b.with_checked_history(h, counts))); - } - // The pieces' shells in order: their faces in order, their edges themselves. - let mut faces = Some(Vec::new()); - let mut edges = HashMap::new(); - for (p, h) in &pieces { - let (Some(h), Some(all)) = (h, faces.as_mut()) else { - faces = None; - break; - }; - all.extend(h.faces.iter().cloned()); - edges.extend(record::edges_by_identity(p, h)); - } - if let Some(faces) = faces.as_mut() { - record::mark_shared_split(faces); - } - let joined = crate::ops::join_pieces(pieces.into_iter().map(|(p, _)| p).collect())?; - Ok(joined.map(|body| record::traced(body, counts, faces, Some(&edges)))) -} - -/// A truck boolean's history in our terms, in the order of the solid it made: each face a -/// region of one operand face (Kept when that very face), each edge's origins. -pub(crate) type Shapeops = (Vec, Vec>); - -/// A truck boolean's history in our terms ([`Shapeops`]). `tool` is the -/// tool as passed (after a cut turned it inside out, which keeps its faces' order but walks each -/// face's edges the other way, so its edge order changes) and `tool_edges` its edges by identity -/// in its own order (before turning). -pub(crate) fn shapeops(h: &truck_shapeops::History, tool: &Solid, tool_edges: Option<&HashMap>>) -> Option { - let face = |f: truck_shapeops::FaceIndex| FaceOrigin::Face { operand: f.operand, face: f.face }; - let faces = - h.faces.iter().map(|o| record::from_face(o.source.operand, o.source.face, if o.kept { Change::Kept } else { Change::Modified })).collect(); - let passed: Vec = edge_order(tool.face_iter()).0.iter().map(|e| e.id()).collect(); - let edges = h - .edges - .iter() - .map(|o| match *o { - truck_shapeops::EdgeOrigin::Edge { operand: 1, edge } if tool_edges.is_some() => { - tool_edges.and_then(|t| t.get(passed.get(edge)?)).cloned() - } - truck_shapeops::EdgeOrigin::Edge { operand, edge } => Some(vec![EdgeOrigin::Edge { operand, edge }]), - truck_shapeops::EdgeOrigin::Meet { a, b } => Some(vec![EdgeOrigin::meet(face(a), face(b))]), - }) - .collect::>>()?; - Some((faces, edges)) -} - -/// Samples along each edge when checking that its curve evaluates. -const EVALUATION_SAMPLES: usize = 32; - -/// Whether every edge of `solid` evaluates along its whole range. An intersection curve projects -/// its leader's point onto both surfaces, which fails to converge where a boolean made the edge -/// from an approximation that strayed (#1252). Such a result is no solid to keep: the mesher, -/// healing and measuring all sample the edge and stumble over the gap. -pub(crate) fn edges_evaluate(solid: &Solid) -> bool { - use truck_meshalgo::prelude::TrySubs; - use truck_modeling::BoundedCurve; - solid.boundaries().iter().flat_map(|shell| shell.edge_iter()).all(|edge| { - let curve = edge.curve(); - let (t0, t1) = curve.range_tuple(); - (0..=EVALUATION_SAMPLES).all(|k| { - let t = t0 + (t1 - t0) * k as f64 / EVALUATION_SAMPLES as f64; - curve.try_subs(t).is_some_and(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite()) - }) - }) -} - -/// The body made of `solid` healed (keeping faces apart along `keep`), with the history whose -/// faces and edges (in `solid`'s orders) are given before healing, carried through it. -/// Operands with `counts` faces and edges. A result with an edge that does not evaluate is -/// refused (see [`edges_evaluate`]). -pub(crate) fn heal_build( - solid: Solid, - size: f64, - keep: &[Vec], - faces: Option>, - edges: Option>>, - counts: &[(usize, usize)], -) -> Result { - let (healed, map) = crate::heal::heal_traced(solid, size, keep); - if !edges_evaluate(&healed) { - return Err(crate::KernelError::Failed("an edge of the result does not evaluate".into())); - } - let carried = faces.zip(edges).zip(map).and_then(|((f, e), m)| m.carry(&f, &e)); - let (faces, edges) = carried.unzip(); - record::build(healed, counts, faces, edges.as_ref()) -} - -/// The body truck's result `s` makes, healed (see [`heal_build`]), with its history `h`. -pub(crate) fn truck_result(s: Solid, h: Option, size: f64, keep: &[Vec], counts: &[(usize, usize)]) -> Result { - let (faces, edges) = h.unzip(); - heal_build(s, size, keep, faces, edges, counts) -} - -/// `body` healed (see [`heal_build`]), its history carried through: `solid` is `body`'s solid -/// or a copy of it (`Body::deep_copy`, which keeps its order). -pub(crate) fn heal_body(body: &Body, solid: Solid, size: f64, keep: &[Vec], counts: &[(usize, usize)]) -> Result { - let (faces, edges) = body.history().map(|h| (h.faces.clone(), h.edges.iter().map(|e| e.origins.clone()).collect())).unzip(); - heal_build(solid, size, keep, faces, edges, counts) -} - -/// A prism boolean's result `body` with the coplanar pieces its slabs leave merged (nothing -/// else healed: its edges are exact as they are), its history carried through, and what else it carries kept: each face's -/// construction source where every piece merged into it had that same one, its Split Face -/// lines and its regularization protection. -pub(crate) fn heal_prism_result(body: &Body, size: f64, keep: &[Vec], counts: &[(usize, usize)]) -> Result { - let (faces, edges): (Option>, Option>>) = - body.history().map(|h| (h.faces.clone(), h.edges.iter().map(|e| e.origins.clone()).collect())).unzip(); - let (healed, map) = crate::heal::merge_faces_traced((*body.solid).clone(), size, keep); - let sources = match (body.face_sources(), &map) { - (Some(sources), Some(m)) => Some( - m.faces - .iter() - .map(|under| { - let mut each = under.iter().map(|i| sources.get(*i).cloned().flatten()); - let first = each.next().flatten()?; - each.all(|s| s.as_ref() == Some(&first)).then_some(first) - }) - .collect::>(), - ), - _ => None, - }; - let carried = faces.zip(edges).zip(map).and_then(|((f, e), m)| m.carry(&f, &e)); - let (faces, edges) = carried.unzip(); - let mut out = record::build(healed, counts, faces, edges.as_ref())?; - out = out.with_face_sources(sources.map(std::sync::Arc::new)).inherit_regularization_protection(body); - out.splits = body.splits.clone(); - Ok(out) -} - -/// `body` healed as the local modifiers take it (coplanar neighbours as one face, as after a -/// join; faces split on purpose kept apart), with its history over `[body]`. +/// `body` healed as the local modifiers take it (faces on one surface as one face, as after a +/// join; faces split on purpose kept apart), with its history over `[body]` +/// ([`crate::heal::heal`]), each face keeping the construction source its faces shared and the +/// body its regularization protection. pub(crate) fn healed(body: &Body) -> Result { - let counts = record::counts(&[body]); - heal_body(&operand(body, 0, &counts), body.deep_copy(), body.size(), body.split_keep(), &counts) -} - -/// `body` copied as it is, unhealed (`Body::deep_copy`, which keeps its face and edge order), -/// with its history over `[body]`: every face and edge kept. -pub(crate) fn copied(body: &Body) -> Result { - let copy = Body::new(body.deep_copy())?; - Ok(operand(©, 0, &record::counts(&[body]))) -} - -/// The faces and edges of `a`, then of the tool `b` turned inside out as a void (`inner`, a -/// copy of `b`, with `inner_edges` its edges by identity before turning): the history of -/// `a` with `b`'s inside taken out where `b` lies wholly within it. -pub(crate) fn void( - a: &Body, - inner_edges: HashMap>, - b_faces: usize, -) -> (Vec, HashMap>) { - let faces = (0..a.face_count()) - .map(|i| record::from_face(0, i, Change::Kept)) - .chain((0..b_faces).map(|i| record::from_face(1, i, Change::Kept))) - .collect(); - let mut edges = record::edge_origins(&a.solid, 0); - edges.extend(inner_edges); - (faces, edges) + let healed = crate::heal::heal(body, body.split_keep())?; + let sources = healed.history().map(|h| std::sync::Arc::new(h.face_sources(&[body]))); + Ok(healed.with_face_sources(sources).inherit_regularization_protection(body)) } #[cfg(test)]