A cut built slab by slab (the exact prism boolean) sewed the result from horizontal slabs and only joins merged the coplanar pieces back. So a floor vent cut through the Raspberry Pi case split the long wall into strips at the ports' tops and bottoms (87 faces instead of 79 at 32 mm), and at 24 mm the end opening split the end wall. - Prism booleans merge coplanar neighbours for every operation (faces only: edges, curves and surfaces stay exact), keeping each face's construction source where all its pieces share it, Split Face lines and regularization protection. Painted or protected operands keep their authored pieces as before. - The prism history now tells apart a wall's several edges with its caps (a wall with openings meets each opening's floor), so cuts beside existing openings keep exact history and faces keep their names. - Tests: the Pi case's faces pinned after every feature at 24, at 24 changed to 32, and built at 32 (each outer side one face); random openings and bosses on a shelled case never split another side; every corpus design rebuilds a parameter edit like a fresh build and reverts exactly. - Action matrix baseline: demo/pi-case re-blessed (fewer, merged faces; no regressions), demo/pi-case-0.8 recorded, and 88 new passes on regressions/lid_ribs_flush_with_window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
212 lines
10 KiB
Rust
212 lines
10 KiB
Rust
//! 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;
|
|
|
|
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());
|
|
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<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(face(a), 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)
|
|
}
|
|
|
|
/// 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<Vec3>], counts: &[(usize, usize)]) -> Result<Body> {
|
|
let (faces, edges): (Option<Vec<FaceProv>>, Option<Vec<Vec<EdgeOrigin>>>) =
|
|
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::<Vec<_>>(),
|
|
),
|
|
_ => 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]`.
|
|
pub(crate) fn healed(body: &Body) -> Result<Body> {
|
|
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<Body> {
|
|
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<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"]
|
|
pub(crate) mod tests;
|