feat(kernel): every boolean path reports exact face and edge history
ops::boolean's result carries its history over [a, b] whichever path made it, recorded by the code that builds the faces: - truck path (incl. jitter retries): shapeops history mapped through the order-preserving copies and the cut's turned tool, then through healing - heal records which input faces each healed face merges and which input edges each joined or exact edge replaces (HealMap); opt-in, so heal_keep callers pay nothing - trivial shortcuts, the void, glue, faces pushed apart (push_faces records its own history; the slab cut back on a union maps to the operands' faces in that plane), the nudged retry, by_lumps and join composition - prism booleans: section segments carry tags (operand cap edge, the wall beside it, its vertices) through the 2D boolean; walls, caps and edges of the extruded or sewn result follow from them - planar and faceted polygon booleans: polygons carry their operand face; faces join the pieces across edges, edges come from the pieces either side History gains renumber/over (and consuming forms) for composition; record gains regions/traced/build helpers. truck-shapeops *_with_history now return the solid with an Option history. Paths that cannot report (bore narrowing via offset, inexpressible lineage) leave the result without history. Boolean results, tolerances and fallback order are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
1 parent
d69ebbee1b
commit
d5cb276965
16 files changed
+2169
-312
No files matched your search
@@ -0,0 +1,170 @@
|
||||
//! 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 solvecraft_geom::Vec3;
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::Result;
|
||||
use crate::body::{Body, Solid, edge_order};
|
||||
use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv, History};
|
||||
use crate::record;
|
||||
|
||||
/// A composition step's history, or none (logged) where the lineage can't be expressed.
|
||||
pub(crate) fn composed(h: Result<History>) -> Option<History> {
|
||||
h.map_err(|e| log::debug!("no history: {e}")).ok()
|
||||
}
|
||||
|
||||
/// `h`, the history of an operation over one operand, as over `operands` with that one at
|
||||
/// `operand`.
|
||||
pub(crate) fn placed(h: Option<History>, operand: usize, operands: usize) -> Option<History> {
|
||||
composed(h?.renumbered(&[Some(operand)], operands))
|
||||
}
|
||||
|
||||
/// The history of `body` as operand `operand` of `operands`: every face and edge kept.
|
||||
pub(crate) fn kept(body: &Body, operand: usize, operands: usize) -> Option<History> {
|
||||
placed(Some(History::identity(body)), operand, operands)
|
||||
}
|
||||
|
||||
/// The history of `result`, made from operands of which operand `k` was made by the operation
|
||||
/// `inner` describes, with `inner`'s operands in `k`'s place.
|
||||
pub(crate) fn through(result: &Body, k: usize, inner: Option<&History>) -> Option<History> {
|
||||
composed(result.history()?.clone().substituted(k, inner?))
|
||||
}
|
||||
|
||||
/// Operand `operand` (with `counts` giving every operand's faces and edges) as the result, with
|
||||
/// the history that says so.
|
||||
pub(crate) fn operand(body: &Body, operand: usize, counts: &[(usize, usize)]) -> Body {
|
||||
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<Option<History>> {
|
||||
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::<Option<Vec<_>>>()?;
|
||||
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<History>);
|
||||
|
||||
/// The history over the operation's operands of `result`, made from `x` and `y` (in that order)
|
||||
/// whose histories over them are given.
|
||||
pub(crate) fn of_pair(result: &Body, x: &Option<History>, y: &Option<History>) -> Option<History> {
|
||||
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<Traced>, counts: &[(usize, usize)]) -> Result<Option<Body>> {
|
||||
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());
|
||||
for (e, prov) in edge_order(p.solid.face_iter()).0.iter().zip(&h.edges) {
|
||||
edges.insert(e.id(), prov.origins.clone());
|
||||
}
|
||||
}
|
||||
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<FaceProv>, Vec<Vec<EdgeOrigin>>);
|
||||
|
||||
/// 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<mt::EdgeID, Vec<EdgeOrigin>>>) -> Option<Shapeops> {
|
||||
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<mt::EdgeID> = 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 { a: face(a), b: face(b) }]),
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some((faces, edges))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub(crate) fn heal_build(
|
||||
solid: Solid,
|
||||
size: f64,
|
||||
keep: &[Vec<Vec3>],
|
||||
faces: Option<Vec<FaceProv>>,
|
||||
edges: Option<Vec<Vec<EdgeOrigin>>>,
|
||||
counts: &[(usize, usize)],
|
||||
) -> Result<Body> {
|
||||
let (healed, map) = crate::heal::heal_traced(solid, size, keep);
|
||||
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<Shapeops>, size: f64, keep: &[Vec<Vec3>], counts: &[(usize, usize)]) -> Result<Body> {
|
||||
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<Vec3>], counts: &[(usize, usize)]) -> Result<Body> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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<mt::EdgeID, Vec<EdgeOrigin>>,
|
||||
b_faces: usize,
|
||||
) -> (Vec<FaceProv>, HashMap<mt::EdgeID, Vec<EdgeOrigin>>) {
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "boolean_history_tests.rs"]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user