feat(kernel): modifiers report exact face and edge history

Fillets (local, loop, rolling ball, corner, variable, chord), chamfers
(cutters, local corners, loops, two sides), shell, draft, offset faces,
split face/body, delete face, patch/copy/stitch/trim/extend and
transforms record their history over [body] while they build: faces
rebuilt from the body's, blends in place of edges (corner patches in
place of the edges at the corner), shell walls as offsets, edges as
pieces of the body's or where two faces meet. Multi-step operations
compose each step. The push-face offset fallback now has history, and
the bore union composes its narrowed tool.

A reflection reorders a body's edges, so transforms now match edges by
identity (mirrored copies reported the wrong edges).

Refs #136

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 d5cb276965
commit 08d7037176
19 files changed
+1146 -231

No files matched your search

+100 -31
View File
@@ -13,6 +13,8 @@ use solvecraft_geom::Vec3;
use truck_modeling::{self as mt, builder};
use crate::body::{Body, Solid, from_p3, p3, v3};
use crate::history::{FaceOrigin, History};
use crate::record::{self, Rebuild};
use crate::{KernelError, Result, guard};
#[derive(Clone, Copy, PartialEq)]
@@ -89,7 +91,8 @@ fn rebuild_face_dropping(f: &mt::Face, subst: &HashMap<mt::EdgeID, mt::Edge>, dr
crate::heal::absolute_face(f, wires).ok_or_else(|| KernelError::Failed("blend: a face could not be rebuilt".into()))
}
fn blend_one(solid: &Solid, edge: &mt::Edge, size: f64, r: f64, shape: Shape) -> Result<Solid> {
/// Blend one edge of `solid`, with what was built from it (see [`Rebuild`]).
fn blend_one(solid: &Solid, edge: &mt::Edge, size: f64, r: f64, shape: Shape) -> Result<(Solid, Rebuild)> {
if !matches!(edge.curve(), mt::Curve::Line(_)) {
return Err(unsupported("only straight edges can be blended"));
}
@@ -203,7 +206,7 @@ fn blend_geometry(
size: f64,
r: f64,
shape: Shape,
) -> Result<Solid> {
) -> Result<(Solid, Rebuild)> {
let s = setback(r, phi, shape);
let tol = (size * 1e-7).max(1e-9);
// End faces and the side edges at each end. A side edge that the fillet's setback uses up
@@ -351,6 +354,9 @@ fn blend_geometry(
subst.insert(e.id(), ne);
}
}
// The side edges at the ends are pieces of themselves; the rails and corner curves are new.
let mut rec = Rebuild::default();
rec.pieces(&subst);
let mut new_faces: Vec<mt::Face> = Vec::with_capacity(faces.len() + 1);
for (i, f) in faces.iter().enumerate() {
if (i == i1 && rail_a.is_some()) || (i == i2 && rail_b.is_some()) {
@@ -371,6 +377,7 @@ fn blend_geometry(
} else {
(*f).clone()
};
rec.face(&nf, f);
new_faces.push(nf);
}
// The blend face uses each new edge opposite to its neighbour.
@@ -399,13 +406,15 @@ fn blend_geometry(
mt::Face::try_new(vec![wire], surf).map_err(|e| KernelError::Failed(format!("fillet face: {e}")))?
}
};
rec.blend(&blend_face, [edge.id()]);
new_faces.push(blend_face);
let mut shells = solid.boundaries().clone();
let new_shell: mt::Shell = new_faces.into();
if let Some(sh) = shells.get_mut(si) {
*sh = new_shell;
}
Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("blend produced an invalid solid: {e}")))
let solid = Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("blend produced an invalid solid: {e}")))?;
Ok((solid, rec))
}
fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Result<Body> {
@@ -418,8 +427,10 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
if edges.len() > 1000 {
return Err(KernelError::Invalid("too many edges".into()));
}
// Each step's result carries its history over `[body]`.
let counts = record::counts(&[body]);
// Coplanar neighbours as one face (as after a join), so corners look as they should.
let mut cur = Body::new(crate::heal::heal_keep(body.deep_copy(), body.size(), body.split_keep()))?;
let mut cur = crate::boolean_record::healed(body)?;
// Every edge of a convex planar body: rounded as a whole (sphere corners).
if shape == Shape::Round && edges.len() >= 6 {
let solid = cur.deep_copy();
@@ -436,7 +447,7 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
if hit.len() == all.len()
&& let Ok(b) = crate::polyhedron::round_all_edges(&cur, r)
{
return Ok(b);
return Ok(record::after(b, &cur, &counts));
}
}
}
@@ -449,8 +460,8 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
{
let _ = groups;
let loop_pts: Vec<Vec3> = edges.iter().filter(|p| !rest.iter().any(|q| q.dist(**p) < 1e-12)).copied().collect();
cur = blend(&cur, &rest, r, shape, what)?;
return blend(&cur, &loop_pts, r, shape, what);
cur = record::after(blend(&cur, &rest, r, shape, what)?, &cur, &counts);
return Ok(record::after(blend(&cur, &loop_pts, r, shape, what)?, &cur, &counts));
}
if let Some(groups) = loop_groups(&cur, edges)? {
for g in groups {
@@ -458,7 +469,10 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
let solid = cur.deep_copy();
let ids = edge_ids(&cur, &solid, &g)?;
match crate::loopblend::loop_blend(&solid, &ids, size, r, shape == Shape::Round) {
Some(res) => cur = Body::new(guard(what, || res)?)?,
Some(res) => {
let (made, rec) = guard(what, || res)?;
cur = record::after(record::rebuilt(made, &[&solid], &rec)?, &cur, &counts);
}
None => return Err(unsupported("the edges are not a whole loop of a planar face")),
}
}
@@ -477,10 +491,10 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
let solid = cur.deep_copy();
if todo.len() >= 2
&& let Some((ids, covered)) = run_on_loop(&cur, &solid, &todo)?
&& let Some(Ok(res)) = crate::loopblend::loop_blend(&solid, &ids, size, r, shape == Shape::Round)
&& let Ok(b) = guard(what, || Ok(res)).and_then(Body::new)
&& let Some(Ok((res, rec))) = crate::loopblend::loop_blend(&solid, &ids, size, r, shape == Shape::Round)
&& let Ok(b) = guard(what, || Ok(res)).and_then(|res| record::rebuilt(res, &[&solid], &rec))
{
cur = b;
cur = record::after(b, &cur, &counts);
todo.retain(|p| !covered.iter().any(|q| q.dist(*p) < 1e-12));
continue;
}
@@ -517,7 +531,7 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
None => Err(e),
},
};
match attempt.and_then(|(o, js)| Body::new(o).map(|b| (b, js))) {
match attempt.and_then(|((o, rec), js)| record::rebuilt(o, &[&solid], &rec).map(|b| (b, js))) {
Ok(x) => {
done = Some(x);
break;
@@ -531,7 +545,7 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
}
match done {
Some((b, js)) => {
cur = b;
cur = record::after(b, &cur, &counts);
let mut k = 0;
todo.retain(|_| {
let keep = !js.contains(&k);
@@ -810,15 +824,21 @@ pub fn chamfer(body: &Body, edges: &[Vec3], distance: f64) -> Result<Body> {
if edges.is_empty() || edges.len() > 1000 {
return Err(KernelError::Invalid("select 1…1000 edges".into()));
}
// Each step's result carries its history over `[body]`.
let counts = record::counts(&[body]);
// All cutters at once (their union mitres the corners), else one edge at a time.
let tools: Result<Vec<Body>> = edges.iter().enumerate().map(|(i, p)| chamfer_tool(body, *p, distance, i)).collect();
let tools: Result<Vec<Cutter>> = edges.iter().enumerate().map(|(i, p)| chamfer_tool(body, *p, distance, i)).collect();
if let Ok(tools) = tools {
let mut acc: Option<Body> = None;
// The union so far, with its history over the cutters it joined.
let mut acc: Option<(Body, Option<History>)> = None;
let mut ok = true;
for t in tools {
for t in &tools {
let next = match &acc {
None => Some(t),
Some(a) => crate::ops::boolean(a, &t, crate::BoolOp::Union).ok().flatten(),
None => Some((t.body.clone(), Some(History::identity(&t.body)))),
Some((a, h)) => crate::ops::boolean(a, &t.body, crate::BoolOp::Union).ok().flatten().map(|u| {
let h = crate::boolean_record::through(&u, 0, h.as_ref());
(u, h)
}),
};
if next.is_none() {
ok = false;
@@ -827,10 +847,11 @@ pub fn chamfer(body: &Body, edges: &[Vec3], distance: f64) -> Result<Body> {
acc = next;
}
if ok
&& let Some(tool) = acc
&& let Some((tool, h)) = acc
&& let Ok(Some(r)) = crate::ops::boolean(body, &tool, crate::BoolOp::Cut)
{
return Ok(r);
let h = cut_history(crate::boolean_record::through(&r, 1, h.as_ref()), body, &tools);
return Ok(r.with_checked_history(h, &counts));
}
}
// At a pocket rim the cutters cannot extend beyond the selected edges. Rebuild
@@ -842,28 +863,49 @@ pub fn chamfer(body: &Body, edges: &[Vec3], distance: f64) -> Result<Body> {
return Ok(result);
}
// Coplanar neighbours as one face (as after a join), so corners look as they should.
let mut cur = Body::new(crate::heal::heal_keep(body.deep_copy(), body.size(), body.split_keep()))?;
let mut cur = crate::boolean_record::healed(body)?;
if let Some(groups) = loop_groups(&cur, edges)? {
for g in groups {
let size = cur.size();
let solid = cur.deep_copy();
let ids = edge_ids(&cur, &solid, &g)?;
match crate::loopblend::loop_blend(&solid, &ids, size, distance, false) {
Some(res) => cur = Body::new(guard("chamfer", || res)?)?,
Some(res) => {
let (made, rec) = guard("chamfer", || res)?;
cur = record::after(record::rebuilt(made, &[&solid], &rec)?, &cur, &counts);
}
None => return Err(unsupported("the edges are not a whole loop of a planar face")),
}
}
return Ok(cur);
}
for p in edges {
cur = match chamfer_tool(&cur, *p, distance, 0).and_then(|t| crate::ops::boolean(&cur, &t, crate::BoolOp::Cut)) {
let step = match chamfer_tool(&cur, *p, distance, 0).and_then(|t| cut_one(&cur, &t)) {
Ok(Some(b)) => b,
_ => blend(&cur, std::slice::from_ref(p), distance, Shape::Flat, "chamfer")?,
};
cur = record::after(step, &cur, &counts);
}
Ok(cur)
}
/// `cur` cut by one cutter, with its history over `[cur]` (`None`: nothing left).
fn cut_one(cur: &Body, t: &Cutter) -> Result<Option<Body>> {
let counts = record::counts(&[cur]);
Ok(crate::ops::boolean(cur, &t.body, crate::BoolOp::Cut)?.map(|r| {
let h = cut_history(r.history().cloned(), cur, std::slice::from_ref(t));
r.with_checked_history(h, &counts)
}))
}
/// A cut of `cur` by cutters (their union), from its history `h` over `[cur, cutter 0, …]`, as
/// over `[cur]`: each cutter's chamfer face is made in place of its edge, and nothing else of a
/// cutter may be left (`None` where something is).
fn cut_history(h: Option<History>, cur: &Body, tools: &[Cutter]) -> Option<History> {
let parts: Vec<History> = std::iter::once(History::identity(cur)).chain(tools.iter().map(Cutter::history)).collect();
crate::boolean_record::composed(h?.over(&parts.iter().collect::<Vec<_>>()))
}
/// Geometry of a straight convex edge between two planar faces.
struct EdgeFrame {
p0: Vec3,
@@ -875,7 +917,8 @@ struct EdgeFrame {
t2: Vec3,
}
fn edge_frame(cur: &Body, p: Vec3) -> Result<EdgeFrame> {
/// The edge nearest `p` (its index in `cur`'s edge order) and its frame.
fn edge_frame(cur: &Body, p: Vec3) -> Result<(usize, EdgeFrame)> {
let size = cur.size();
let tol = (size * 2e-3).max(1e-3);
let Some((idx, dist)) = cur.nearest_edge(p, tol)? else { return Err(KernelError::Invalid("the body has no edges".into())) };
@@ -903,7 +946,7 @@ fn edge_frame(cur: &Body, p: Vec3) -> Result<EdgeFrame> {
if n1.dot(t2) > 1e-9 || n2.dot(t1) > 1e-9 || t1.dot(t2) < -1.0 + 1e-6 {
return Err(unsupported("only convex edges"));
}
Ok(EdgeFrame { p0, p1, d, n1, n2, t1, t2 })
Ok((idx, EdgeFrame { p0, p1, d, n1, n2, t1, t2 }))
}
/// The prism that cuts a chamfer of size `s` on the edge nearest `p`. `k` varies the parts that
@@ -933,20 +976,41 @@ pub fn chamfer_sides(body: &Body, edges: &[Vec3], d1: f64, side: ChamferSide, fl
if edges.is_empty() || edges.len() > 1000 {
return Err(KernelError::Invalid("select 1…1000 edges".into()));
}
let mut cur = body.clone();
let counts = record::counts(&[body]);
let mut cur = crate::boolean_record::operand(body, 0, &counts);
for (k, p) in edges.iter().enumerate() {
let t = chamfer_tool_sides(&cur, *p, d1, Some((side, flip)), k)?;
cur = crate::ops::boolean(&cur, &t, crate::BoolOp::Cut)?.ok_or_else(|| KernelError::Failed("the chamfer removed everything".into()))?;
let step = cut_one(&cur, &t)?.ok_or_else(|| KernelError::Failed("the chamfer removed everything".into()))?;
cur = record::after(step, &cur, &counts);
}
Ok(cur)
}
pub(crate) fn chamfer_tool(cur: &Body, p: Vec3, s: f64, k: usize) -> Result<Body> {
/// The prism that cuts a chamfer: the body, which of its faces is the chamfer plane, and the
/// edge (of the body it chamfers, in its edge order) that face is made in place of.
pub(crate) struct Cutter {
body: Body,
face: Option<usize>,
edge: usize,
}
impl Cutter {
/// The cutter as made from the body it chamfers: its chamfer face in place of the edge, its
/// other faces (all outside that body) from nothing of it.
fn history(&self) -> History {
let faces = (0..self.body.face_count())
.map(|i| record::generated(if Some(i) == self.face { vec![FaceOrigin::Edge { operand: 0, edge: self.edge }] } else { Vec::new() }))
.collect();
record::from_faces(&self.body.solid, 1, faces)
}
}
pub(crate) fn chamfer_tool(cur: &Body, p: Vec3, s: f64, k: usize) -> Result<Cutter> {
chamfer_tool_sides(cur, p, s, None, k)
}
fn chamfer_tool_sides(cur: &Body, p: Vec3, s: f64, sides: Option<(ChamferSide, bool)>, k: usize) -> Result<Body> {
let mut f = edge_frame(cur, p)?;
fn chamfer_tool_sides(cur: &Body, p: Vec3, s: f64, sides: Option<(ChamferSide, bool)>, k: usize) -> Result<Cutter> {
let (edge, mut f) = edge_frame(cur, p)?;
// Setbacks along each face: equal, or by the second side's rule.
let (s1, s2) = match sides {
None => (s, s),
@@ -997,11 +1061,16 @@ fn chamfer_tool_sides(cur: &Body, p: Vec3, s: f64, sides: Option<(ChamferSide, b
}
let loc: Vec<solvecraft_geom::Vec2> = [a_ext, b_ext, e_far].iter().map(|q| plane.to_local(*q)).collect();
let mut lp = solvecraft_geom::Loop2::polygon(&loc);
// The chamfer plane is swept from the segment a → b: first, or last once turned round.
let mut chamfer_segment = 0;
if lp.signed_area() < 0.0 {
lp = lp.reversed();
chamfer_segment = lp.segs.len().saturating_sub(1);
}
let region = solvecraft_geom::Region2 { outer: lp, holes: vec![] };
crate::build::extrude(&plane, &[region], 0.0, len + 2.0 * ext)?.pop().ok_or_else(|| KernelError::Failed("chamfer tool".into()))
let body = crate::build::extrude(&plane, &[region], 0.0, len + 2.0 * ext)?.pop().ok_or_else(|| KernelError::Failed("chamfer tool".into()))?;
let face = body.history().and_then(|h| h.faces.iter().position(|f| f.origins == [FaceOrigin::Profile { segment: chamfer_segment }]));
Ok(Cutter { body, face, edge })
}
#[cfg(test)]
+28 -14
View File
@@ -33,13 +33,15 @@ pub fn fillet_radii(body: &Body, edges: &[(Vec3, f64)]) -> Result<Body> {
if edges.iter().any(|(p, r)| !(p.is_finite() && r.is_finite() && *r > 1e-6 && *r < 1e6)) {
return Err(KernelError::Invalid("fillet size must be positive".into()));
}
let mut cur = Body::new(crate::heal::heal_keep(body.deep_copy(), body.size(), body.split_keep()))?;
// Each step's result carries its history over `[body]`.
let counts = crate::record::counts(&[body]);
let mut cur = crate::boolean_record::healed(body)?;
let mut sel = edges.to_vec();
let mut first: Option<KernelError> = None;
// Earlier rounds at the edges' ends come off and are blended again with the new edges.
for _ in 0..4 {
let err = match build(&cur, &sel, true) {
Ok(b) => return Ok(b),
Ok(b) => return Ok(crate::record::after(b, &cur, &counts)),
Err(e) => e,
};
let rounds = rounds_at(&cur, &sel);
@@ -48,7 +50,7 @@ pub fn fillet_radii(body: &Body, edges: &[(Vec3, f64)]) -> Result<Body> {
}
first.get_or_insert(err);
let idx: Vec<usize> = rounds.iter().map(|r| r.0).collect();
cur = crate::delete_faces(&cur, &idx)?;
cur = crate::record::after(crate::delete_faces(&cur, &idx)?, &cur, &counts);
sel.extend(rounds.iter().map(|r| (r.1, r.2)));
}
Err(first.unwrap_or_else(|| unsupported("these fillets")))
@@ -57,9 +59,9 @@ pub fn fillet_radii(body: &Body, edges: &[(Vec3, f64)]) -> Result<Body> {
/// Local equal-distance chamfers, including pocket rims where adjacent walls have
/// different slopes. The planar strips trim each other at each selected corner.
pub(crate) fn chamfer(body: &Body, edges: &[Vec3], distance: f64) -> Result<Body> {
let cur = Body::new(crate::heal::heal_keep(body.deep_copy(), body.size(), body.split_keep()))?;
let cur = crate::boolean_record::healed(body)?;
let selected: Vec<_> = edges.iter().map(|p| (*p, distance)).collect();
build(&cur, &selected, false)
Ok(crate::record::after(build(&cur, &selected, false)?, &cur, &crate::record::counts(&[body])))
}
/// A face of the shell being rebuilt, or a new face.
@@ -311,7 +313,8 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
// New end points: (edge, vertex, face for a filleted edge's contact line).
let mut endpt: HashMap<(mt::EdgeID, mt::VertexID, Option<usize>), mt::Vertex> = HashMap::new();
let mut pool: HashMap<mt::VertexID, Vec<(mt::Edge, Vec<Owner>)>> = HashMap::new();
let mut patches: Vec<mt::Face> = Vec::new();
// Corner patches, with the edges meeting at their corner.
let mut patches: Vec<(mt::Face, [mt::EdgeID; 3])> = Vec::new();
let face_with = |a: mt::EdgeID, b: mt::EdgeID| -> Result<usize> {
let fa = on_edge.get(&a).cloned().unwrap_or_default();
let fb = on_edge.get(&b).cloned().unwrap_or_default();
@@ -440,7 +443,8 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
let c3 = edge(&v23, &v31, mt::Curve::NurbsCurve(t3))?;
let surf = cp.surf;
let wire: mt::Wire = vec![c2.clone(), c3.clone(), c1.inverse()].into();
patches.push(mt::Face::try_new(vec![wire], surf).map_err(|e| KernelError::Failed(format!("corner patch: {e}")))?);
patches
.push((mt::Face::try_new(vec![wire], surf).map_err(|e| KernelError::Failed(format!("corner patch: {e}")))?, [*e1, *e2, *e3]));
list.push((c1, vec![Owner::Cyl(*e1), Owner::Patch]));
list.push((c2, vec![Owner::Cyl(*e2), Owner::Patch]));
list.push((c3, vec![Owner::Cyl(*e3), Owner::Patch]));
@@ -468,6 +472,7 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
Ok(x)
};
let moved = |e: &mt::Edge| touched.contains_key(&e.absolute_front().id()) || touched.contains_key(&e.absolute_back().id());
let mut rec = crate::record::Rebuild::default();
let mut new_faces: Vec<mt::Face> = Vec::with_capacity(faces.len() + sels.len() + patches.len());
for (fi, f) in faces.iter().enumerate() {
if !f.vertex_iter().any(|v| touched.contains_key(&v.id())) {
@@ -482,7 +487,10 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
let ne = if sels.contains_key(&e.id()) {
line_of(e, Some(fi))?
} else if moved(e) {
line_of(e, None)?
// The edge itself, shortened at the corner.
let piece = line_of(e, None)?;
rec.piece(&piece, e);
piece
} else {
news.push(e.clone());
continue;
@@ -501,7 +509,9 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
}
wires.push(mt::Wire::from(out));
}
new_faces.push(mt::Face::try_new(wires, f.oriented_surface()).map_err(|e| KernelError::Failed(format!("fillet: face: {e}")))?);
let rebuilt = mt::Face::try_new(wires, f.oriented_surface()).map_err(|e| KernelError::Failed(format!("fillet: face: {e}")))?;
rec.face(&rebuilt, f);
new_faces.push(rebuilt);
}
// The blends: back along the first face's contact line, round the corner, along the
// second's, round the other corner.
@@ -518,10 +528,13 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
w.extend(connect(first.back(), second.front(), p1, Owner::Cyl(*id))?);
w.push(second.clone());
w.extend(connect(second.back(), first.front(), p2, Owner::Cyl(*id))?);
new_faces.push(mt::Face::try_new(vec![w.into()], s.surf.clone()).map_err(|e| KernelError::Failed(format!("fillet face: {e}")))?);
let blend = mt::Face::try_new(vec![w.into()], s.surf.clone()).map_err(|e| KernelError::Failed(format!("fillet face: {e}")))?;
rec.blend(&blend, [*id]);
new_faces.push(blend);
}
// Corner patches run each section opposite to its blend.
for mut p in patches {
// Corner patches run each section opposite to its blend; each is made in place of the
// edges meeting at its corner.
for (mut p, corner) in patches {
let uses = |f: &mt::Face, id: mt::EdgeID| {
f.boundaries().iter().flat_map(|w| w.edge_iter().cloned().collect::<Vec<_>>()).find(|x| x.id() == id).map(|x| x.front().id())
};
@@ -535,14 +548,15 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result<Body> {
let wires: Vec<mt::Wire> = p.boundaries().iter().map(|w| w.inverse()).collect();
p = mt::Face::try_new(wires, surf).map_err(|e| KernelError::Failed(format!("corner patch: {e}")))?;
}
rec.blend(&p, corner);
new_faces.push(p);
}
let mut shells = solid.boundaries().clone();
if let Some(sh) = shells.get_mut(si) {
*sh = new_faces.into();
}
let out = Body::new(Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("fillet produced an invalid solid: {e}")))?)?;
Ok(out)
let out = Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("fillet produced an invalid solid: {e}")))?;
crate::record::rebuilt(out, &[&solid], &rec)
})
}
+11 -3
View File
@@ -229,6 +229,9 @@ pub fn fillet_variable_points(body: &Body, edge_at: Vec3, start: Vec3, points: &
subst.insert(e.id(), ne);
}
}
// The side edges at the ends are pieces of themselves; the contacts and end arcs are new.
let mut rec = crate::record::Rebuild::default();
rec.pieces(&subst);
let mut new_faces: Vec<mt::Face> = Vec::with_capacity(faces.len() + 1);
for (i, f) in faces.iter().enumerate() {
let nf = if i == fr.i1 {
@@ -246,6 +249,7 @@ pub fn fillet_variable_points(body: &Body, edge_at: Vec3, start: Vec3, points: &
} else {
(*f).clone()
};
rec.face(&nf, f);
new_faces.push(nf);
}
// The blend surface: rational quadratic arcs (tangent points and the edge point, middle
@@ -279,12 +283,14 @@ pub fn fillet_variable_points(body: &Body, edge_at: Vec3, start: Vec3, points: &
vec![la.clone(), c1.clone(), lb.inverse(), c0.inverse()].into()
};
let blend = mt::Face::try_new(vec![wire], surf).map_err(|e| KernelError::Failed(format!("fillet face: {e}")))?;
rec.blend(&blend, [edge.id()]);
new_faces.push(blend);
let mut shells = solid.boundaries().clone();
if let Some(sh) = shells.get_mut(si) {
*sh = new_faces.into();
}
Body::new(Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("variable fillet produced an invalid solid: {e}")))?)
let out = Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("variable fillet produced an invalid solid: {e}")))?;
crate::record::rebuilt(out, &[solid], &rec)
})
}
@@ -312,9 +318,11 @@ pub fn fillet_chord(body: &Body, edges: &[Vec3], chord: f64) -> Result<Body> {
None => groups.push((r, vec![*p])),
}
}
let mut cur = body.clone();
// Each step's result carries its history over `[body]`.
let counts = crate::record::counts(&[body]);
let mut cur = crate::boolean_record::operand(body, 0, &counts);
for (r, pts) in groups {
cur = crate::fillet(&cur, &pts, r)?;
cur = crate::record::after(crate::fillet(&cur, &pts, r)?, &cur, &counts);
}
Ok(cur)
}
+6
View File
@@ -480,6 +480,12 @@ impl Body {
self
}
/// This body with no history: an operation whose history is not known.
pub(crate) fn without_history(mut self) -> Self {
self.history = None;
self
}
pub fn face_sources(&self) -> Option<&[Option<FaceSource>]> {
self.face_sources.as_deref().map(Vec::as_slice)
}
+44 -27
View File
@@ -48,7 +48,7 @@ fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
}
/// Distance from `p` to face `f` of a mesh.
fn to_face(m: &Mesh, f: usize, p: Vec3) -> f64 {
pub(crate) fn to_face(m: &Mesh, f: usize, p: Vec3) -> f64 {
m.triangles
.iter()
.zip(&m.tri_face)
@@ -61,34 +61,51 @@ fn to_face(m: &Mesh, f: usize, p: Vec3) -> f64 {
}
/// Distance from `p` to edge `e` of a mesh.
fn to_edge(m: &Mesh, e: usize, p: Vec3) -> f64 {
pub(crate) fn to_edge(m: &Mesh, e: usize, p: Vec3) -> f64 {
m.edges[e].windows(2).map(|w| p.dist_to_segment(w[0], w[1])).fold(f64::INFINITY, f64::min)
}
/// How far, in a check, a face or edge may lie from what it came from.
#[derive(Clone, Copy)]
pub(crate) struct Reach {
/// A face made in place of an edge, or an offset of a face, lies within this of it.
pub made: f64,
/// A face or edge the operation moved lies within this of the one it came from.
pub moved: f64,
}
/// The result's history, checked against it and its operands, with every origin checked
/// against the geometry: each face's triangles lie on its origin faces, each origin face holds
/// some of them; each edge lies along its origin edges, or on both faces of a meeting. `tol`:
/// the meshes' tolerance.
fn verified<'a>(r: &'a Body, operands: &[&Body], tol: f64) -> &'a History {
let h = r.history().expect("the boolean reports its history");
checked(r, operands, tol, Reach { made: 0.0, moved: 0.0 }, &|_| false)
}
/// [`verified`] for any operation: a face lies on its origin faces (within `at.moved` where the
/// operation moves faces), within `at.made` of the edges it is made in place of and of the faces
/// it is an offset of; an edge lies along its origin edges (within `at.moved`). Faces from
/// nothing are allowed only where `generated` says.
pub(crate) fn checked<'a>(r: &'a Body, operands: &[&Body], tol: f64, at: Reach, generated: &dyn Fn(usize) -> bool) -> &'a History {
let h = r.history().expect("the operation reports its history");
h.check(r, operands).unwrap();
let rm = r.tessellate(tol).unwrap();
let ms: Vec<Mesh> = operands.iter().map(|b| b.tessellate(tol).unwrap()).collect();
let near = 4.0 * tol + r.size() * 1e-6;
let on = |o: &FaceOrigin, p: Vec3| match *o {
FaceOrigin::Face { operand, face } => to_face(&ms[operand], face, p) < near,
FaceOrigin::Face { operand, face } => to_face(&ms[operand], face, p) < near.max(at.moved),
FaceOrigin::Edge { operand, edge } => to_edge(&ms[operand], edge, p) < at.made + near,
FaceOrigin::Offset { operand, face } => to_face(&ms[operand], face, p) < at.made + near,
_ => false,
};
let along = |o: &EdgeOrigin, p: &Vec3| match *o {
EdgeOrigin::Edge { operand, edge } => to_edge(&ms[operand], edge, *p) < near.max(at.moved),
EdgeOrigin::Meet { a, b } => on(&a, *p) && on(&b, *p),
};
for (i, f) in h.faces.iter().enumerate() {
assert!(!f.origins.is_empty(), "face {i} has no origin");
let centres: Vec<Vec3> = rm
.triangles
.iter()
.zip(&rm.tri_face)
.filter(|(_, x)| **x as usize == i)
.map(|(t, _)| t.map(|k| rm.positions[k as usize]).iter().fold(Vec3::ZERO, |s, p| s + *p) * (1.0 / 3.0))
.collect();
for c in &centres {
assert!(!f.origins.is_empty() || generated(i), "face {i} has no origin");
let centres = centres(&rm, i);
for c in centres.iter().filter(|_| !f.origins.is_empty()) {
assert!(f.origins.iter().any(|o| on(o, *c)), "face {i} ({f:?}) at {c:?} lies on none of its origins");
}
for o in &f.origins {
@@ -98,33 +115,33 @@ fn verified<'a>(r: &'a Body, operands: &[&Body], tol: f64) -> &'a History {
for (j, e) in h.edges.iter().enumerate() {
let pts = &rm.edges[j];
for o in &e.origins {
let ok = |p: &Vec3| match *o {
EdgeOrigin::Edge { operand, edge } => to_edge(&ms[operand], edge, *p) < near,
EdgeOrigin::Meet { a, b } => on(&a, *p) && on(&b, *p),
};
match o {
// A joined edge runs along all of its origins, each along part of it.
EdgeOrigin::Edge { .. } if e.origins.len() > 1 => assert!(pts.iter().any(ok), "edge {j}: {o:?} is not along it"),
_ => assert!(pts.iter().all(ok), "edge {j} ({e:?}) does not lie along {o:?}"),
EdgeOrigin::Edge { .. } if e.origins.len() > 1 => assert!(pts.iter().any(|p| along(o, p)), "edge {j}: {o:?} is not along it"),
_ => assert!(pts.iter().all(|p| along(o, p)), "edge {j} ({e:?}) does not lie along {o:?}"),
}
}
if e.origins.len() > 1 {
for p in pts {
assert!(
e.origins.iter().any(|o| match *o {
EdgeOrigin::Edge { operand, edge } => to_edge(&ms[operand], edge, *p) < near,
EdgeOrigin::Meet { a, b } => on(&a, *p) && on(&b, *p),
}),
"edge {j}: {p:?} lies along none of {e:?}"
);
assert!(e.origins.iter().any(|o| along(o, p)), "edge {j}: {p:?} lies along none of {e:?}");
}
}
}
h
}
/// The centres of face `f`'s triangles.
pub(crate) fn centres(m: &Mesh, f: usize) -> Vec<Vec3> {
m.triangles
.iter()
.zip(&m.tri_face)
.filter(|(_, x)| **x as usize == f)
.map(|(t, _)| t.map(|k| m.positions[k as usize]).iter().fold(Vec3::ZERO, |s, p| s + *p) * (1.0 / 3.0))
.collect()
}
/// How many faces of each change.
fn changes(h: &History) -> [usize; 5] {
pub(crate) fn changes(h: &History) -> [usize; 5] {
let mut n = [0; 5];
for f in &h.faces {
n[match f.change {
+15 -1
View File
@@ -148,6 +148,20 @@ pub(crate) fn heal_body(body: &Body, solid: Solid, size: f64, keep: &[Vec<Vec3>]
heal_build(solid, size, keep, faces, edges, counts)
}
/// `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(&copy, 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.
@@ -167,4 +181,4 @@ pub(crate) fn void(
#[cfg(test)]
#[path = "boolean_history_tests.rs"]
mod tests;
pub(crate) mod tests;
+33 -7
View File
@@ -43,7 +43,7 @@ fn planes(b: &Body) -> Vec<(usize, PlaneFace)> {
/// Push the planar faces of `b` lying in the plane (n, d) — n their outward normal — along n
/// by `delta` (negative: inward). Their neighbours must stand perpendicular to the plane. The
/// result's history is over `[b]` (none where the faces had to be offset instead).
/// result's history is over `[b]`.
pub(crate) fn push_faces(b: &Body, n: Vec3, d: f64, delta: f64) -> Result<Body> {
// The exact rebuild, unless it came out unsound (a curved side it couldn't re-sweep).
match push_faces_exact(b, n, d, delta)
@@ -66,7 +66,6 @@ pub(crate) fn push_faces(b: &Body, n: Vec3, d: f64, delta: f64) -> Result<Body>
_ => false,
})
.collect();
// An offset reports no history yet.
crate::offset::offset_body(b, |i, _| if planes.get(i).copied().unwrap_or(false) { delta } else { 0.0 }).map_err(|_| e)
}
}
@@ -539,6 +538,29 @@ fn same_cylinder(x: &CylFace, y: &CylFace, tol: f64) -> bool {
x.axis.dot(y.axis).abs() > 1.0 - 1e-9 && (x.radius - y.radius).abs() < tol && (off - x.axis * off.dot(x.axis)).len() < tol
}
/// The history over `[a, b]` of `r`, made in [`bore_union`]: `joined` (the target joined with
/// `narrowed`, the tool with its bore narrowed) less `cut`, the cylinder whose wall restores the
/// tool's bore faces `bore` (the tool is operand `tool_operand`). Nothing of the cylinder but its
/// wall may be left.
fn recut(r: &Body, joined: &Body, narrowed: &Body, cut: &Body, bore: &[usize], tool_operand: usize) -> Option<History> {
// Over [target, tool].
let joined = crate::boolean_record::through(joined, 1, narrowed.history())?;
let wall = || record::region(bore.iter().map(|&face| FaceOrigin::Face { operand: 1, face }).collect(), false);
let faces = cut
.history()?
.faces
.iter()
.map(|f| match f.origins.first() {
Some(FaceOrigin::Profile { .. }) => wall(),
_ => record::generated(Vec::new()),
})
.collect();
let cut = record::from_faces(&cut.solid, 2, faces);
let h = crate::boolean_record::of_pair(r, &Some(joined), &Some(cut))?;
let map = if tool_operand == 1 { [Some(0), Some(1)] } else { [Some(1), Some(0)] };
crate::boolean_record::composed(h.renumbered(&map, 2))
}
/// Do the bodies have cylindrical faces on a common cylinder?
pub(crate) fn has_coincident_cylinders(a: &Body, b: &Body) -> bool {
let tol = (a.size().max(b.size()) * 1e-7).max(1e-9) * 10.0;
@@ -554,7 +576,8 @@ pub(crate) fn has_coincident_cylinders(a: &Body, b: &Body) -> bool {
/// samples are plausibility checks; a whole-boundary guard rejects additional boundaries
/// crossing the recut. Original common-bore trims retain the legacy eligibility policy.
/// This is not a general emptiness or Boolean-equivalence certificate. Each direction
/// returns a result only when `accept` takes it; otherwise this returns `None`.
/// returns a result only when `accept` takes it; otherwise this returns `None`. The result's
/// history is over `[a, b]`.
pub(crate) fn bore_union(a: &Body, b: &Body, accept: &dyn Fn(&Body) -> bool) -> Option<Body> {
let size = a.size().max(b.size());
let tol = (size * 1e-7).max(1e-9) * 10.0;
@@ -563,9 +586,10 @@ pub(crate) fn bore_union(a: &Body, b: &Body, accept: &dyn Fn(&Body) -> bool) ->
let (ia, ib) = (ma.inside_index(), mb.inside_index());
let empty = |p: Vec3| !ia.contains(p) && !ib.contains(p);
let (ca, cb) = (cylinders(a), cylinders(b));
// (body to narrow, its mesh and inside test, the other body, their faces on the cylinder).
let ways = [(b, &mb, &ib, a, &cb, &ca), (a, &ma, &ia, b, &ca, &cb)];
for (tool, mesh, inside, target, ct, co) in ways {
// (body to narrow, its operand number, its mesh and inside test, the other body, their faces
// on the cylinder).
let ways = [(b, 1, &mb, &ib, a, &cb, &ca), (a, 0, &ma, &ia, b, &ca, &cb)];
for (tool, tool_operand, mesh, inside, target, ct, co) in ways {
for c in ct {
if !co.iter().any(|o| same_cylinder(c, o, tol)) {
continue;
@@ -644,7 +668,9 @@ pub(crate) fn bore_union(a: &Body, b: &Body, accept: &dyn Fn(&Body) -> bool) ->
}
let Some(joined) = crate::ops::boolean(target, &narrowed, BoolOp::Union)? else { return Ok(None) };
let cut = crate::build::cylinder(at(lo, 0.0, 0.0), c.axis, c.radius, hi - lo)?;
crate::ops::boolean(&joined, &cut, BoolOp::Cut)
let Some(r) = crate::ops::boolean(&joined, &cut, BoolOp::Cut)? else { return Ok(None) };
let h = recut(&r, &joined, &narrowed, &cut, &bore, tool_operand);
Ok(Some(r.with_checked_history(h, &record::counts(&[a, b]))))
};
let (result, diagnostics) = crate::capture_boolean_diagnostics(run);
match result {
+15 -6
View File
@@ -12,6 +12,7 @@ use solvecraft_geom::Vec3;
use truck_modeling::{self as mt, builder};
use crate::body::{Solid, from_p3, p3};
use crate::record::Rebuild;
use crate::{KernelError, Result};
fn unsupported(msg: &str) -> KernelError {
@@ -316,12 +317,13 @@ fn trim_curved_seam(seam: &mt::Edge, a: Option<mt::Vertex>, b: Option<mt::Vertex
mt::Edge::try_new(&a, &b, piece).map_err(|e| KernelError::Failed(format!("curve blend: seam: {e}")))
}
/// Blend the closed chain of edges (ids in any order) with radius `r`.
pub(crate) fn curve_blend(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64, round: bool) -> Result<Solid> {
/// Blend the closed chain of edges (ids in any order) with radius `r`; with what it built from
/// `solid` (each blend face in place of its chain edge).
pub(crate) fn curve_blend(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64, round: bool) -> Result<(Solid, Rebuild)> {
curve_blend_with_limit(solid, ids, size, r, round, MAX_SAMPLES)
}
fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64, round: bool, max_samples: usize) -> Result<Solid> {
fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64, round: bool, max_samples: usize) -> Result<(Solid, Rebuild)> {
if !(size.is_finite() && size > 0.0 && r.is_finite() && r > 0.0) {
return Err(KernelError::Invalid("curve blend size and radius must be finite and positive".into()));
}
@@ -696,6 +698,8 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
seam_subst.insert(id, builder::line(&a, &b));
}
let mut out: Vec<mt::Face> = Vec::new();
let mut rec = Rebuild::default();
rec.pieces(&seam_subst);
for (fi, f) in faces.iter().enumerate() {
let corners: Vec<usize> = end_faces.iter().filter(|(_, g)| *g == fi).map(|(k, _)| *k).collect();
if !corners.is_empty() {
@@ -734,7 +738,9 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
}
wires.push(mt::Wire::from(es));
}
out.push(crate::heal::absolute_face(f, wires).ok_or_else(|| fail("an end face could not be rebuilt"))?);
let rebuilt = crate::heal::absolute_face(f, wires).ok_or_else(|| fail("an end face could not be rebuilt"))?;
rec.face(&rebuilt, f);
out.push(rebuilt);
continue;
}
let on1 = side1.contains(&fi);
@@ -761,7 +767,9 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
}
wires.push(mt::Wire::from(es));
}
out.push(crate::heal::absolute_face(f, wires).ok_or_else(|| fail("a side face could not be rebuilt"))?);
let rebuilt = crate::heal::absolute_face(f, wires).ok_or_else(|| fail("a side face could not be rebuilt"))?;
rec.face(&rebuilt, f);
out.push(rebuilt);
}
// Blend faces: each runs its side-1 contact the other way from side 1's face.
for (k, bf) in blends.iter().enumerate() {
@@ -785,13 +793,14 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
vec![e1.clone(), ak1.clone(), e2.inverse(), ak.inverse()].into()
};
let face = mt::Face::try_new(vec![wire], bf.clone()).map_err(|e| fail(&e.to_string()))?;
rec.blend(&face, [chain_e.id()]);
out.push(face);
}
let mut shells = solid.boundaries().clone();
if let Some(sh) = shells.get_mut(si) {
*sh = out.into();
}
Solid::try_new(shells).map_err(|e| fail(&format!("invalid solid: {e}")))
Ok((Solid::try_new(shells).map_err(|e| fail(&format!("invalid solid: {e}")))?, rec))
})
}
+9 -3
View File
@@ -55,7 +55,7 @@ fn key(p: Vec3, q: f64) -> Key {
}
/// Delete faces (indices in the body's face order, as in its mesh's `tri_face`) and heal the
/// gap by extending the neighbouring planes.
/// gap by extending the neighbouring planes. The result's history is over `[body]`.
pub fn delete_faces(body: &Body, faces: &[usize]) -> Result<Body> {
body.require_brep("Delete Face")?;
let all: Vec<mt::Face> = body.solid.face_iter().cloned().collect();
@@ -194,6 +194,9 @@ pub fn delete_faces(body: &Body, faces: &[usize]) -> Result<Body> {
lines.insert((ka, kb), e.clone());
e
};
// Neighbours rebuilt from themselves, moved edges pieces of themselves; the edges that
// close the gap are new.
let mut rec = crate::record::Rebuild::default();
let mut out: Vec<mt::Face> = Vec::new();
for (fi, f) in all.iter().enumerate() {
if del.contains(&fi) {
@@ -256,7 +259,9 @@ pub fn delete_faces(body: &Body, faces: &[usize]) -> Result<Body> {
new_edges.push(e.clone());
} else if matches!(e.oriented_curve(), mt::Curve::Line(_)) {
if a.dist(b) > q * 10.0 {
new_edges.push(line(a, b, &mut verts));
let moved = line(a, b, &mut verts);
rec.piece(&moved, e);
new_edges.push(moved);
}
} else {
return Err(fail(format!("face {fi} has a curved edge that would have to stretch")));
@@ -285,11 +290,12 @@ pub fn delete_faces(body: &Body, faces: &[usize]) -> Result<Body> {
}
_ => face,
};
rec.face(&face, f);
out.push(face);
}
let shell: mt::Shell = out.into();
let solid = Solid::try_new(vec![shell]).map_err(|e| fail(format!("the neighbouring faces do not close the gap ({e})")))?;
Body::new(solid)
crate::record::rebuilt(solid, &[&body.solid], &rec)
})
}
+4 -8
View File
@@ -407,12 +407,7 @@ fn planar_surfaces(shell: &mt::Shell, tol: f64) -> Option<mt::Shell> {
/// Heal a solid; returns the input unchanged when nothing applies or healing fails.
#[cfg(test)]
pub fn heal(solid: Solid, size: f64) -> Solid {
heal_keep(solid, size, &[])
}
/// [`heal`], keeping faces apart along lines they were split along on purpose.
pub(crate) fn heal_keep(solid: Solid, size: f64, keep: &[Vec<Vec3>]) -> Solid {
healed(solid, size, keep, false).0
healed(solid, size, &[], false).0
}
/// What healing made each face and edge of its result from.
@@ -467,8 +462,9 @@ impl HealMap {
}
}
/// [`heal_keep`], with what each face and edge of the result was made from (`None` when that
/// could not be followed).
/// Heal a solid (the input unchanged when nothing applies or healing fails), keeping faces apart
/// along lines they were split along on purpose (`keep`), with what each face and edge of the
/// result was made from (`None` when that could not be followed).
pub(crate) fn heal_traced(solid: Solid, size: f64, keep: &[Vec<Vec3>]) -> (Solid, Option<HealMap>) {
healed(solid, size, keep, true)
}
+2
View File
@@ -40,6 +40,8 @@ mod loopblend;
mod measure;
mod mesh_convert;
mod meshbody;
#[cfg(test)]
mod modify_history_tests;
mod numbered;
mod offset;
mod ops;
+19 -10
View File
@@ -11,6 +11,7 @@ use solvecraft_geom::Vec3;
use truck_modeling::{self as mt, builder};
use crate::body::{Solid, from_p3, p3, v3};
use crate::record::Rebuild;
use crate::{KernelError, Result};
fn unsupported(msg: &str) -> KernelError {
@@ -141,8 +142,9 @@ fn face_normal(f: &mt::Face, p: Vec3) -> Option<Vec3> {
}
/// Blend the loop of `edges` (by id) if it is a whole smooth boundary loop of a planar face with
/// perpendicular walls. `None` when the edges are not such a loop.
pub(crate) fn loop_blend(solid: &Solid, edge_ids: &[mt::EdgeID], size: f64, r: f64, round: bool) -> Option<Result<Solid>> {
/// perpendicular walls, with what it built from `solid` (each blend face in place of its loop
/// edge). `None` when the edges are not such a loop.
pub(crate) fn loop_blend(solid: &Solid, edge_ids: &[mt::EdgeID], size: f64, r: f64, round: bool) -> Option<Result<(Solid, Rebuild)>> {
let tol = (size * 1e-6).max(1e-7);
for (si, shell) in solid.boundaries().iter().enumerate() {
let faces: Vec<&mt::Face> = shell.face_iter().collect();
@@ -184,7 +186,7 @@ fn build(
r: f64,
round: bool,
run: Option<(usize, usize)>,
) -> Result<Solid> {
) -> Result<(Solid, Rebuild)> {
let f = faces.get(fi).ok_or_else(|| KernelError::Failed("face".into()))?;
let wire = f.boundaries().get(wi).cloned().ok_or_else(|| KernelError::Failed("loop".into()))?;
let all_edges: Vec<mt::Edge> = wire.edge_iter().cloned().collect();
@@ -550,17 +552,21 @@ fn build(
crate::heal::absolute_face(g, wires).ok_or_else(|| KernelError::Failed("loop blend: a face could not be rebuilt".into()))
};
let mut out: Vec<mt::Face> = Vec::new();
let mut rec = Rebuild::default();
rec.pieces(&seam_subst);
for (gi, g) in faces.iter().enumerate() {
if gi == fi {
out.push(rebuild(g, &face_rails)?);
let rebuilt = if gi == fi {
rebuild(g, &face_rails)?
} else if walls.contains(&gi) {
let wr: Vec<Option<mt::Edge>> = wall_rails.iter().cloned().map(Some).collect();
out.push(rebuild(g, &wr)?);
rebuild(g, &wr)?
} else if g.edge_iter().any(|e| seam_subst.contains_key(&e.id())) {
out.push(rebuild(g, &[])?);
rebuild(g, &[])?
} else {
out.push((*g).clone());
}
(*g).clone()
};
rec.face(&rebuilt, g);
out.push(rebuilt);
}
// Blend faces.
use mt::{ParametricSurface3D, SearchNearestParameter};
@@ -612,11 +618,14 @@ fn build(
None => vec![pr.clone(), wr.clone(), pr_next.inverse()].into(),
};
let face = mt::Face::try_new(vec![w.clone()], surf.clone()).map_err(|e| KernelError::Failed(format!("blend face: {e}")))?;
// In place of the loop edge this piece runs along (a full circle's two halves both).
rec.blend(&face, owner.get(i).and_then(|o| edges.get(*o)).map(|e| e.id()));
out.push(face);
}
let mut shells = solid.boundaries().clone();
if let Some(sh) = shells.get_mut(si) {
*sh = out.into();
}
Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("loop blend produced an invalid solid: {e}")))
let solid = Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("loop blend produced an invalid solid: {e}")))?;
Ok((solid, rec))
}
+378
View File
@@ -0,0 +1,378 @@
//! Modifiers' histories (fillets, chamfers, shell, draft, offset, split, delete, surfaces): every
//! face and edge origin checked against the geometry of the operands (in tests only: the history
//! itself is recorded, never sampled), and the exact origins asserted where the outcome is known.
use solvecraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
use crate::body::Body;
use crate::boolean_record::tests::{self, Reach, changes, to_edge, to_face};
use crate::history::{Change, EdgeOrigin, FaceOrigin, History};
use crate::{BoolOp, boolean, box_solid, cylinder, extrude};
const TOL: f64 = 0.05;
const fn face(face: usize) -> FaceOrigin {
FaceOrigin::Face { operand: 0, face }
}
const fn edge(edge: usize) -> FaceOrigin {
FaceOrigin::Edge { operand: 0, edge }
}
const fn reach(made: f64) -> Reach {
Reach { made, moved: 0.0 }
}
/// [`checked`] at this module's mesh tolerance.
fn checked<'a>(r: &'a Body, operands: &[&Body], at: Reach, generated: &dyn Fn(usize) -> bool) -> &'a History {
tests::checked(r, operands, TOL, at, generated)
}
/// [`checked`] for a modifier of one operand whose every face comes from something of it.
fn verified<'a>(r: &'a Body, operand: &Body, at: Reach) -> &'a History {
checked(r, &[operand], at, &|_| false)
}
/// The faces whose origins are exactly `origins`.
fn with(h: &History, origins: &[FaceOrigin]) -> Vec<usize> {
(0..h.faces.len()).filter(|i| h.faces[*i].origins == origins).collect()
}
/// The origins of every face made in place of edges (blends).
fn blends(h: &History) -> Vec<Vec<FaceOrigin>> {
h.faces.iter().filter(|f| f.origins.iter().any(|o| matches!(o, FaceOrigin::Edge { .. }))).map(|f| f.origins.clone()).collect()
}
/// The index of the edge of `b` nearest `p`.
fn edge_at(b: &Body, p: Vec3) -> usize {
b.nearest_edge(p, TOL).unwrap().unwrap().0
}
/// The index of the face of `b` holding `p`.
fn face_at(b: &Body, p: Vec3) -> usize {
let m = b.tessellate(TOL).unwrap();
(0..b.face_count()).min_by(|x, y| to_face(&m, *x, p).total_cmp(&to_face(&m, *y, p))).unwrap()
}
fn cube() -> Body {
box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap()
}
fn square(x0: f64, y0: f64, x1: f64, y1: f64) -> Loop2 {
Loop2::polygon(&[Vec2::new(x0, y0), Vec2::new(x1, y0), Vec2::new(x1, y1), Vec2::new(x0, y1)])
}
/// An L-shaped prism (not convex), 20 high.
fn ell() -> Body {
let outer = Loop2::polygon(&[
Vec2::ZERO,
Vec2::new(40.0, 0.0),
Vec2::new(40.0, 15.0),
Vec2::new(15.0, 15.0),
Vec2::new(15.0, 30.0),
Vec2::new(0.0, 30.0),
]);
extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0.0, 20.0).unwrap().pop().unwrap()
}
#[test]
fn fillet_one_edge() {
let b = cube();
let at = Vec3::new(20.0, 30.0, 20.0);
let r = crate::fillet(&b, &[at], 5.0).unwrap();
let h = verified(&r, &b, reach(5.0));
assert_eq!(with(h, &[edge(edge_at(&b, at))]).len(), 1);
// Two faces along the edge and the two at its ends are trimmed, two are untouched.
assert_eq!(changes(h), [2, 4, 0, 0, 1]);
}
#[test]
fn fillet_an_edge_a_cut_split() {
let b = cube();
// A slot through the back leaves two pieces of the top back edge.
let notch = box_solid(Vec3::new(15.0, 25.0, -5.0), Vec3::new(25.0, 35.0, 25.0)).unwrap();
let cut = boolean(&b, &notch, BoolOp::Cut).unwrap().unwrap();
let at = Vec3::new(5.0, 30.0, 20.0);
let piece = edge_at(&cut, at);
assert_eq!(cut.history().unwrap().edges[piece].origins, [EdgeOrigin::Edge { operand: 0, edge: edge_at(&b, at) }]);
let r = crate::fillet(&cut, &[at], 3.0).unwrap();
let h = verified(&r, &cut, reach(3.0));
assert_eq!(blends(h), [vec![edge(piece)]]);
// Through the cut: the blend stands in place of the box's edge, which the cut split.
let mut through = h.clone();
through.substitute(0, cut.history().unwrap()).unwrap();
through.check(&r, &[&b, &notch]).unwrap();
assert_eq!(blends(&through), [vec![edge(edge_at(&b, at))]]);
}
#[test]
fn fillet_three_edges_at_a_corner() {
let b = cube();
let pts = [Vec3::new(20.0, 30.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(40.0, 30.0, 10.0)];
let r = crate::fillet_radii(&b, &pts.map(|p| (p, 4.0))).unwrap();
let h = verified(&r, &b, reach(4.0 * 1.5));
let edges: Vec<usize> = pts.iter().map(|p| edge_at(&b, *p)).collect();
for e in &edges {
assert_eq!(with(h, &[edge(*e)]).len(), 1, "{h:?}");
}
// The corner patch stands in place of the three edges meeting there.
let mut corner: Vec<FaceOrigin> = edges.iter().map(|e| edge(*e)).collect();
corner.sort();
assert_eq!(with(h, &corner).len(), 1, "{:?}", blends(h));
}
#[test]
fn fillet_a_whole_loop() {
let b = cube();
let pts = [Vec3::new(20.0, 0.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(20.0, 30.0, 20.0), Vec3::new(0.0, 15.0, 20.0)];
let r = crate::fillet(&b, &pts, 3.0).unwrap();
let h = verified(&r, &b, reach(3.0 * 1.5));
let mut made: Vec<FaceOrigin> = blends(h).into_iter().flatten().collect();
made.sort();
let mut want: Vec<FaceOrigin> = pts.iter().map(|p| edge(edge_at(&b, *p))).collect();
want.sort();
assert_eq!(made, want);
}
#[test]
fn fillet_every_edge_of_a_box() {
let b = cube();
let pts: Vec<Vec3> = b.edges(TOL).unwrap().iter().map(|e| e.mid).collect();
let r = crate::fillet(&b, &pts, 2.0).unwrap();
let h = verified(&r, &b, reach(2.0 * 2.0));
// Six faces shrunk, twelve cylinders, eight corners of three edges each.
assert_eq!(changes(h), [0, 6, 0, 0, 20]);
assert_eq!(blends(h).iter().filter(|o| o.len() == 1).count(), 12);
assert_eq!(blends(h).iter().filter(|o| o.len() == 3).count(), 8);
}
#[test]
fn fillet_a_rolling_ball_chain() {
let main = cylinder(Vec3::new(-40.0, 0.0, 0.0), Vec3::X, 15.0, 80.0).unwrap();
let branch = cylinder(Vec3::ZERO, Vec3::Z, 10.0, 35.0).unwrap();
let tee = boolean(&main, &branch, BoolOp::Union).unwrap().unwrap();
let at = Vec3::new(0.0, 10.0, 125f64.sqrt());
let r = crate::fillet(&tee, &[at], 3.0).unwrap();
let h = verified(&r, &tee, reach(3.0 * 2.0));
assert!(!blends(h).is_empty());
assert!(blends(h).iter().all(|o| o.len() == 1));
}
#[test]
fn fillet_variable_and_chord() {
let b = cube();
let at = Vec3::new(20.0, 30.0, 20.0);
let r = crate::fillet_variable(&b, at, Vec3::new(0.0, 30.0, 20.0), 2.0, 5.0).unwrap();
let h = verified(&r, &b, reach(5.0));
assert_eq!(blends(h), [vec![edge(edge_at(&b, at))]]);
let r = crate::fillet_chord(&b, &[at, Vec3::new(40.0, 15.0, 0.0)], 4.0).unwrap();
let h = verified(&r, &b, reach(4.0));
assert_eq!(blends(h).len(), 2);
}
#[test]
fn chamfer_cuts_one_edge() {
let b = cube();
let at = Vec3::new(20.0, 30.0, 20.0);
let r = crate::chamfer(&b, &[at], 4.0).unwrap();
let h = verified(&r, &b, reach(4.0));
let chamfer = with(h, &[edge(edge_at(&b, at))]);
assert_eq!(chamfer.len(), 1, "{h:?}");
assert_eq!(h.faces[chamfer[0]].change, Change::Generated);
assert_eq!(r.face_count(), 7);
}
#[test]
fn chamfer_cutters_mitre_at_a_corner() {
let b = cube();
let pts = [Vec3::new(20.0, 30.0, 20.0), Vec3::new(40.0, 15.0, 20.0)];
let r = crate::chamfer(&b, &pts, 3.0).unwrap();
let h = verified(&r, &b, reach(3.0 * 1.5));
for p in pts {
assert_eq!(with(h, &[edge(edge_at(&b, p))]).len(), 1, "{h:?}");
}
// The two chamfers meet along the mitre.
let (a, c) = (edge(edge_at(&b, pts[0])), edge(edge_at(&b, pts[1])));
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::Meet { a, b: c }] || e.origins == [EdgeOrigin::Meet { a: c, b: a }]));
}
#[test]
fn chamfer_with_two_sides() {
let b = cube();
let at = Vec3::new(20.0, 30.0, 20.0);
let r = crate::chamfer_sides(&b, &[at], 2.0, crate::ChamferSide::Distance(4.0), false).unwrap();
let h = verified(&r, &b, reach(4.0));
assert_eq!(blends(h), [vec![edge(edge_at(&b, at))]]);
}
#[test]
fn delete_a_fillet() {
let b = cube();
let r = crate::fillet(&b, &[Vec3::new(20.0, 30.0, 20.0)], 5.0).unwrap();
let round = with(r.history().unwrap(), &[edge(edge_at(&b, Vec3::new(20.0, 30.0, 20.0)))]);
let back = crate::delete_faces(&r, &round).unwrap();
let h = verified(&back, &r, Reach { made: 0.0, moved: 6.0 });
// Every face is one of the filleted body's, trimmed back out to the sharp edge.
assert_eq!(h.faces.len(), 6);
assert!(h.faces.iter().all(|f| f.origins.len() == 1));
// The faces either side of the round meet again where they met nowhere in its input.
let (top, back_face) = (face_at(&r, Vec3::new(20.0, 10.0, 20.0)), face_at(&r, Vec3::new(20.0, 30.0, 5.0)));
let (x, y) = (face(top), face(back_face));
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::Meet { a: x.min(y), b: x.max(y) }]));
}
#[test]
fn shell_lines_the_walls_with_offsets() {
let b = cube();
let top = Vec3::new(20.0, 15.0, 20.0);
let r = crate::shell(&b, &[top], 2.0).unwrap();
let h = verified(&r, &b, reach(2.0));
// Five inner walls, each an offset of the face it lines; the top is left as a rim.
let top = face_at(&b, top);
for f in (0..6).filter(|f| *f != top) {
let walls = with(h, &[FaceOrigin::Offset { operand: 0, face: f }]);
assert_eq!(walls.len(), 1, "{h:?}");
assert_eq!(h.faces[walls[0]].change, Change::Generated);
}
assert!(with(h, &[FaceOrigin::Offset { operand: 0, face: top }]).is_empty());
assert_eq!(changes(h), [0, 6, 0, 0, 5]);
}
#[test]
fn shell_of_a_body_that_is_not_convex() {
let b = ell();
let r = crate::shell(&b, &[Vec3::new(5.0, 5.0, 20.0)], 2.0).unwrap();
// An inner wall runs past its face by the thickness at a concave corner.
let h = verified(&r, &b, reach(2.0 * 1.5));
let offsets = h.faces.iter().filter(|f| matches!(f.origins.as_slice(), [FaceOrigin::Offset { .. }])).count();
assert_eq!(offsets, 7, "{h:?}");
}
#[test]
fn draft_tilts_faces() {
let b = cube();
let side = Vec3::new(40.0, 15.0, 10.0);
let r = crate::draft(&b, &[side], &Plane::XY, Vec3::Z, 5f64.to_radians()).unwrap();
let h = verified(&r, &b, Reach { made: 0.0, moved: 2.0 });
assert_eq!(changes(h), [0, 6, 0, 0, 0]);
assert_eq!(with(h, &[face(face_at(&b, side))]).len(), 1);
// Walls of a body that is not convex.
let l = ell();
let wall = Vec3::new(40.0, 7.0, 10.0);
let r = crate::draft(&l, &[wall], &Plane::XY, Vec3::Z, 5f64.to_radians()).unwrap();
let h = verified(&r, &l, Reach { made: 0.0, moved: 2.0 });
assert_eq!(with(h, &[face(face_at(&l, wall))]).len(), 1);
}
#[test]
fn offset_moves_a_face() {
let b = ell();
let top = Vec3::new(5.0, 5.0, 20.0);
let r = crate::offset_faces(&b, &[top], 3.0).unwrap();
let h = verified(&r, &b, Reach { made: 0.0, moved: 3.0 * 1.1 });
assert_eq!(h.faces.len(), b.face_count());
assert_eq!(with(h, &[face(face_at(&b, top))]).len(), 1);
assert!(h.edges.iter().all(|e| matches!(e.origins.as_slice(), [EdgeOrigin::Edge { .. }])));
}
#[test]
fn split_a_face() {
let b = cube();
let top = face_at(&b, Vec3::new(20.0, 15.0, 20.0));
let plane = Plane::new(Vec3::new(10.0, 0.0, 0.0), Vec3::Y, Vec3::Z).unwrap();
let (r, map) = crate::split_faces_with_map(&b, &[top], &crate::SplitTool::Plane(plane)).unwrap();
let h = verified(&r, &b, reach(0.0));
// Two pieces of the top; the faces whose edges the split cut are whole, as is the rest.
assert_eq!(with(h, &[face(top)]), map.iter().enumerate().filter(|(_, f)| **f == top).map(|(i, _)| i).collect::<Vec<_>>());
assert_eq!(changes(h), [5, 0, 2, 0, 0]);
for (i, f) in map.iter().enumerate() {
assert_eq!(h.faces[i].origins, [face(*f)]);
}
// The split line runs between pieces of one face: it comes from nothing.
assert_eq!(h.edges.iter().filter(|e| e.origins.is_empty()).count(), 1);
}
#[test]
fn split_a_body() {
let b = cube();
let plane = Plane::new(Vec3::new(10.0, 0.0, 0.0), Vec3::Y, Vec3::Z).unwrap();
let parts = crate::split_body(&b, &crate::SplitTool::Plane(plane)).unwrap();
assert_eq!(parts.len(), 2);
for p in &parts {
let h = checked(p, &[&b], reach(0.0), &|i| p.history().is_some_and(|h| h.faces[i].change == Change::Generated));
// One section face, new; the rest the box's faces or pieces of them.
assert_eq!(h.faces.iter().filter(|f| f.origins.is_empty()).count(), 1);
}
}
#[test]
fn surfaces_keep_their_faces() {
let b = cube();
let top = Vec3::new(20.0, 15.0, 20.0);
let copy = crate::copy_faces(&b, &[top]).unwrap();
let h = verified(&copy, &b, reach(0.0));
assert_eq!(h.faces[0].origins, [face(face_at(&b, top))]);
assert_eq!(h.faces[0].change, Change::Kept);
// Trimmed: the copy, cut back (its other piece is gone).
let plane = Plane::new(Vec3::new(10.0, 0.0, 0.0), Vec3::Y, Vec3::Z).unwrap();
let t = crate::trim(&copy, &crate::SplitTool::Plane(plane), Vec3::new(5.0, 15.0, 20.0)).unwrap();
let h = verified(&t, &copy, reach(0.0));
assert_eq!(h.faces[0].origins, [face(0)]);
assert_eq!(h.faces[0].change, Change::Modified);
// Extended: the face and its edges moved out.
let x = crate::extend(&copy, &[Vec3::new(40.0, 15.0, 20.0)], 5.0).unwrap();
let h = verified(&x, &copy, Reach { made: 0.0, moved: 5.0 * 1.1 });
assert_eq!(h.faces[0].origins, [face(0)]);
assert!(h.edges.iter().all(|e| e.origins.len() == 1));
// Patched: a new face on the body's own edges.
let open = crate::copy_faces(
&b,
&[
Vec3::new(20.0, 15.0, 0.0),
Vec3::new(20.0, 0.0, 10.0),
Vec3::new(20.0, 30.0, 10.0),
Vec3::new(0.0, 15.0, 10.0),
Vec3::new(40.0, 15.0, 10.0),
],
)
.unwrap();
let rim = [Vec3::new(20.0, 0.0, 20.0), Vec3::new(40.0, 15.0, 20.0), Vec3::new(20.0, 30.0, 20.0), Vec3::new(0.0, 15.0, 20.0)];
let lid = crate::patch_edges(&open, &rim).unwrap();
let h = checked(&lid, &[&open], reach(0.0), &|_| true);
assert_eq!(h.faces[0].change, Change::Generated);
assert!(h.edges.iter().all(|e| matches!(e.origins.as_slice(), [EdgeOrigin::Edge { operand: 0, .. }])));
// Stitched: each face the one it was sewn from, the seam the edges sewn together.
let closed = crate::stitch(&[&open, &lid], 0.0).unwrap();
let h = checked(&closed, &[&open, &lid], reach(0.0), &|_| false);
assert!(h.faces.iter().all(|f| f.change == Change::Kept && f.origins.len() == 1));
assert_eq!(h.edges.iter().filter(|e| e.origins.len() == 2).count(), 4);
let patch = crate::patch_region(&Plane::XY, &Region2 { outer: square(0.0, 0.0, 10.0, 10.0), holes: vec![] }).unwrap();
assert_eq!(patch.history().unwrap().operands, 0);
}
#[test]
fn thicken_offsets_the_surface() {
let s = crate::patch_region(&Plane::XY, &Region2 { outer: square(0.0, 0.0, 10.0, 10.0), holes: vec![] }).unwrap();
let t = crate::thicken(&s, 2.0, false).unwrap();
let h = verified(&t, &s, reach(2.0));
assert_eq!(with(h, &[face(0)]).len(), 1);
assert_eq!(with(h, &[FaceOrigin::Offset { operand: 0, face: 0 }]).len(), 1);
assert_eq!(blends(h).len(), 4);
}
#[test]
fn mirrored_copy_keeps_every_face_and_edge() {
let b = ell();
let m = [[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [100.0, 0.0, 0.0, 1.0]];
let r = crate::transform_matrix(&b, m).unwrap();
let h = r.history().unwrap();
h.check(&r, &[&b]).unwrap();
// Each edge is the mirror image of the one it says it is.
let (rm, bm) = (r.tessellate(TOL).unwrap(), b.tessellate(TOL).unwrap());
for (j, e) in h.edges.iter().enumerate() {
let [EdgeOrigin::Edge { edge, .. }] = e.origins[..] else { panic!("edge {j}: {e:?}") };
for p in &rm.edges[j] {
assert!(to_edge(&bm, edge, Vec3::new(100.0 - p.x, p.y, p.z)) < 1e-6, "edge {j} is not the mirror of edge {edge}");
}
}
}
+17 -5
View File
@@ -649,7 +649,9 @@ pub(crate) fn arc_mid(e: &mt::Edge, tol: f64) -> Option<Option<Vec3>> {
}
/// The body with each face moved along its outward normal by `shift(face index, normal at a
/// face point)`, keeping the topology. Faces must be planes or cylinders.
/// face point)`, keeping the topology. Faces must be planes or cylinders. The result's history
/// is over `[b]`: each face is the face it was moved from, each edge the edge it was moved from;
/// faces moved to nothing are gone, and their neighbours meet along new edges.
pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Result<Body> {
let faces: Vec<mt::Face> = b.solid.face_iter().cloned().collect();
let size = b.size();
@@ -900,6 +902,8 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
let mut links: HashMap<(mt::VertexID, usize, usize), mt::Edge> = HashMap::new();
// Old edge → new edge, and whether it runs the other way (merged with its twin).
let mut edges: HashMap<mt::EdgeID, (mt::Edge, bool)> = HashMap::new();
// Moved faces and edges; an edge where neighbours of a collapsed face meet is new.
let mut rec = crate::record::Rebuild::default();
let mut merged: HashMap<(mt::VertexID, mt::VertexID), mt::Edge> = HashMap::new();
for e in b.solid.edge_iter() {
if edges.contains_key(&e.id()) {
@@ -949,7 +953,9 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
}
pts.push(from_p3(c.point()));
let curve = crate::build::interpolate_cubic(&pts).ok_or_else(|| fail("edge curve"))?;
edges.insert(e.id(), (mt::Edge::new(a, c, mt::Curve::BSplineCurve(curve)), false));
let ne = mt::Edge::new(a, c, mt::Curve::BSplineCurve(curve));
rec.piece(&ne, &e);
edges.insert(e.id(), (ne, false));
continue;
};
let ne = match kind {
@@ -962,6 +968,7 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
builder::circle_arc(a, c, p3(m2))
}
};
rec.piece(&ne, &e);
edges.insert(e.id(), (ne, false));
}
let mut out = Vec::new();
@@ -1027,6 +1034,7 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
if !f.orientation() {
nf.invert();
}
rec.face(&nf, f);
out.push(nf);
}
// One shell per shell of the body (pieces, voids), less the collapsed faces.
@@ -1039,14 +1047,15 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
shells.push(it.by_ref().take(keep).collect::<Vec<_>>().into());
}
let solid = Solid::try_new(shells).map_err(|e| fail(&e.to_string()))?;
Body::new(solid)
crate::record::rebuilt(solid, &[&b.solid], &rec)
})
}
/// Draft walls parallel to `pull` (planes, and cylinders about axes along `pull`) by `angle`
/// about their line on the neutral plane: at height h along `pull` a wall moves along its
/// outward normal by −h·tan(angle), so planes tilt and cylinders become cones. The other
/// faces must be caps square to `pull`; the topology is kept.
/// faces must be caps square to `pull`; the topology is kept, and the result's history is over
/// `[b]` (each face and edge the one it was moved from).
pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solvecraft_geom::Plane, pull: Vec3, angle: f64) -> Result<Body> {
let faces: Vec<mt::Face> = b.solid.face_iter().cloned().collect();
let size = b.size();
@@ -1166,6 +1175,7 @@ pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solvecraft_geom:
guard("draft", || {
let verts: HashMap<mt::VertexID, mt::Vertex> = newpos.iter().map(|(k, p)| (*k, builder::vertex(p3(*p)))).collect();
let mut edges: HashMap<mt::EdgeID, mt::Edge> = HashMap::new();
let mut rec = crate::record::Rebuild::default();
for e in b.solid.edge_iter() {
if edges.contains_key(&e.id()) {
continue;
@@ -1181,6 +1191,7 @@ pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solvecraft_geom:
builder::circle_arc(a, c, p3(m2))
}
};
rec.piece(&ne, &e);
edges.insert(e.id(), ne);
}
let mut out = Vec::new();
@@ -1234,11 +1245,12 @@ pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solvecraft_geom:
nf
}
};
rec.face(&nf, f);
out.push(nf);
}
let shell: mt::Shell = out.into();
let solid = Solid::try_new(vec![shell]).map_err(|e| fail(&e.to_string()))?;
Body::new(solid)
crate::record::rebuilt(solid, &[&b.solid], &rec)
})
}
+31 -20
View File
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
use solvecraft_geom::Vec3;
use truck_modeling as mt;
use std::collections::HashMap;
use crate::body::{Body, Solid, from_p3};
use crate::boolean_diagnostics::{BooleanFallback, BooleanStrategy, capture_boolean_diagnostics, merge, record};
use crate::boolean_record::{self as recorded, Traced};
@@ -401,8 +403,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
BORES.with(|c| c.set(false));
if let Some(body) = r {
merge(diagnostics);
// Narrowing a bore (an offset) reports no history yet.
return Ok(Some(body.with_checked_history(None, &counts)));
return Ok(Some(body));
}
}
let mut empty_votes = 0;
@@ -696,22 +697,28 @@ pub fn transform_matrix(body: &Body, m: [[f64; 4]; 4]) -> Result<Body> {
if m.iter().flatten().any(|x| !x.is_finite()) {
return Err(KernelError::Invalid("non-finite transform".into()));
}
let placed = match body.mesh_transformed(&m) {
Some(b) => b,
None => guard("transform", || {
let mat = mt::Matrix4::new(
m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1],
m[3][2], m[3][3],
);
// An affine placement retains face order and appearance metadata.
let sources = body.transformed_sources(m)?;
Body::new(placed_topology(&body.solid, mat)?).map(|b| {
b.with_color(body.color()).with_paint(body.paint().cloned()).with_face_sources(sources).inherit_regularization_protection(body)
})
})?,
};
let history = crate::history::History::identity(&placed);
placed.with_history(history, &[body])
if let Some(placed) = body.mesh_transformed(&m) {
let history = crate::history::History::identity(&placed);
return placed.with_history(history, &[body]);
}
guard("transform", || {
let mat = mt::Matrix4::new(
m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2],
m[3][3],
);
// An affine placement retains face order and appearance metadata. Its edges are the
// body's by identity: a reflection turns the solid inside out, which walks each face's
// edges the other way and so changes the edge order.
let sources = body.transformed_sources(m)?;
let (solid, copies) = placed_topology_mapped(&body.solid, mat)?;
let mut rec = record::Rebuild::default();
rec.pieces(&copies);
for (copy, face) in solid.face_iter().zip(body.solid.face_iter()) {
rec.whole(copy, face);
}
let placed = record::rebuilt(solid, &[&body.solid], &rec)?;
Ok(placed.with_color(body.color()).with_paint(body.paint().cloned()).with_face_sources(sources).inherit_regularization_protection(body))
})
}
/// `source` moved by `matrix`, keeping its face and edge order (transforms report an identity
@@ -719,8 +726,12 @@ pub fn transform_matrix(body: &Body, m: [[f64; 4]; 4]) -> Result<Body> {
/// builders: a STEP periodic face may traverse a seam edge twice, and those reject that wire
/// while cloning even though placement preserves its topology.
pub(crate) fn placed_topology(source: &Solid, matrix: mt::Matrix4) -> Result<Solid> {
placed_topology_mapped(source, matrix).map(|(solid, _)| solid)
}
/// [`placed_topology`], with each edge of `source` (by identity) and its copy.
fn placed_topology_mapped(source: &Solid, matrix: mt::Matrix4) -> Result<(Solid, HashMap<mt::EdgeID, mt::Edge>)> {
use mt::{Transform, Transformed};
use std::collections::HashMap;
let mut vertices = HashMap::new();
let mut edges = HashMap::new();
let mut shells = Vec::new();
@@ -771,7 +782,7 @@ pub(crate) fn placed_topology(source: &Solid, matrix: mt::Matrix4) -> Result<Sol
}
shells.push(mt::Shell::from(faces));
}
Ok(Solid::new_unchecked(shells))
Ok((Solid::new_unchecked(shells), edges))
}
#[cfg(test)]
+148 -32
View File
@@ -9,6 +9,8 @@ use solvecraft_geom::{Plane, Vec3};
use truck_modeling::{self as mt, builder};
use crate::body::{Body, Solid, from_p3, p3, v3};
use crate::history::{EdgeOrigin, FaceOrigin, FaceProv, History};
use crate::record;
use crate::{KernelError, Result, guard};
/// A half-space `n · x <= d` (n is the outward unit normal).
@@ -75,7 +77,13 @@ fn combinatorics(hs: &[HalfSpace]) -> Result<(Vec<Vec3>, Vec<(usize, Vec<usize>)
/// The convex polyhedron bounded by the half-spaces.
pub fn convex_polyhedron(hs: &[HalfSpace]) -> Result<Body> {
polyhedron(hs).map(|(b, _)| b)
}
/// [`convex_polyhedron`], with the half-space each of its faces lies on (in face order).
fn polyhedron(hs: &[HalfSpace]) -> Result<(Body, Vec<usize>)> {
let (verts, faces_idx) = combinatorics(hs)?;
let on: Vec<usize> = faces_idx.iter().map(|(hi, _)| *hi).collect();
guard("polyhedron", || {
let tv: Vec<mt::Vertex> = verts.iter().map(|v| builder::vertex(p3(*v))).collect();
let mut edges: HashMap<(usize, usize), mt::Edge> = HashMap::new();
@@ -102,25 +110,27 @@ pub fn convex_polyhedron(hs: &[HalfSpace]) -> Result<Body> {
}
let shell: mt::Shell = faces.into();
let solid = Solid::try_new(vec![shell]).map_err(|e| KernelError::Failed(format!("polyhedron: {e}")))?;
Body::new(solid)
Ok((Body::new(solid)?, on))
})
}
/// The half-spaces of a convex body with only planar faces, or an error.
pub fn planar_convex(b: &Body) -> Result<Vec<(HalfSpace, Vec3)>> {
/// The half-spaces of a convex body with only planar faces, each with a point on it and the
/// body's faces on its plane (in face order), or an error.
pub fn planar_convex(b: &Body) -> Result<Vec<(HalfSpace, Vec3, Vec<usize>)>> {
b.require_brep("this operation")?;
let tol = (b.size() * 1e-3).max(1e-3);
let faces = b.faces(tol)?;
let mut out: Vec<(HalfSpace, Vec3)> = Vec::new();
let mut out: Vec<(HalfSpace, Vec3, Vec<usize>)> = Vec::new();
for f in &faces {
let n = f.plane_normal.ok_or_else(|| KernelError::Failed("not supported yet: bodies with curved faces".into()))?;
let d = n.dot(f.centroid);
if !out.iter().any(|(h, _)| h.n.dot(n) > 1.0 - 1e-9 && (h.d - d).abs() < tol) {
out.push((HalfSpace { n, d }, f.centroid));
match out.iter_mut().find(|(h, ..)| h.n.dot(n) > 1.0 - 1e-9 && (h.d - d).abs() < tol) {
Some((.., on)) => on.push(f.index),
None => out.push((HalfSpace { n, d }, f.centroid, vec![f.index])),
}
}
let verts: Vec<Vec3> = b.solid.vertex_iter().map(|v| from_p3(v.point())).collect();
for (h, _) in &out {
for (h, ..) in &out {
if verts.iter().any(|v| h.n.dot(*v) > h.d + tol) {
return Err(KernelError::Failed("not supported yet: shell and draft of non-convex bodies".into()));
}
@@ -128,7 +138,59 @@ pub fn planar_convex(b: &Body) -> Result<Vec<(HalfSpace, Vec3)>> {
Ok(out)
}
/// Hollow a convex planar body: walls of `thickness` inside, faces near `open` removed.
/// The faces of `b` on half-space `k`'s plane (`groups[k]`) as the face over them: moved, or
/// merged when several.
fn on_plane(under: &[usize]) -> FaceProv {
record::region(under.iter().map(|&face| FaceOrigin::Face { operand: 0, face }).collect(), false)
}
/// The edges of `b` between faces on two of its planes (`groups[k]`: its faces on plane `k`),
/// by pair of planes (the lower first).
fn plane_edges(b: &Body, groups: &[Vec<usize>]) -> HashMap<(usize, usize), Vec<usize>> {
let plane_of: HashMap<usize, usize> = groups.iter().enumerate().flat_map(|(k, g)| g.iter().map(move |f| (*f, k))).collect();
let mut out: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
for (edge, fs) in crate::history::edge_faces(&b.solid).iter().enumerate() {
if let [p, q] = fs[..]
&& let (Some(&x), Some(&y)) = (plane_of.get(&p), plane_of.get(&q))
&& x != y
{
out.entry((x.min(y), x.max(y))).or_default().push(edge);
}
}
out
}
/// The history over `[b]` of a convex polyhedron `poly` whose face `j` lies on the plane of
/// half-space `on[j]`, made from the faces `groups[k]` of `b` on half-space `k`'s plane: each
/// face the faces under it, each edge along the edges of `b` between faces of its two planes,
/// or new where they met nowhere.
fn planes_history(poly: &Body, on: &[usize], groups: &[Vec<usize>], b: &Body) -> Option<History> {
let faces: Vec<FaceProv> = on.iter().map(|k| groups.get(*k).map(|under| on_plane(under))).collect::<Option<_>>()?;
let along = plane_edges(b, groups);
let mut h = record::from_faces(&poly.solid, 1, faces);
for e in &mut h.edges {
if let [x, y] = e.faces[..]
&& let (Some(&kx), Some(&ky)) = (on.get(x), on.get(y))
&& let Some(edges) = along.get(&(kx.min(ky), kx.max(ky)))
{
e.origins = edges.iter().map(|&edge| EdgeOrigin::Edge { operand: 0, edge }).collect();
}
}
Some(h)
}
/// The history over `[b]` of a cavity `poly` whose face `j` lies on half-space `on[j]`, moved
/// in from the faces `groups[k]` of `b`: each face an offset of the faces it lines, each edge new.
fn cavity_history(poly: &Body, on: &[usize], groups: &[Vec<usize>]) -> Option<History> {
let faces = on
.iter()
.map(|k| groups.get(*k).map(|under| record::generated(under.iter().map(|&face| FaceOrigin::Offset { operand: 0, face }).collect())))
.collect::<Option<_>>()?;
Some(record::from_faces(&poly.solid, 1, faces))
}
/// Hollow a convex planar body: walls of `thickness` inside, faces near `open` removed. The
/// result's history is over `[b]`: the inner walls are offsets of the faces they line.
pub fn shell(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
shell_impl(b, open, thickness)?.with_shell_planar_sources(b, thickness, open)
}
@@ -145,16 +207,40 @@ fn shell_impl(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
let size = b.size();
let inner: Vec<HalfSpace> = hs
.iter()
.map(|(h, c)| {
.map(|(h, c, _)| {
let removed = open.iter().any(|p| (h.n.dot(*p) - h.d).abs() < size * 1e-4 + 1e-6 && p.dist(*c) < size * 2.0);
if removed { HalfSpace { n: h.n, d: h.d + thickness + size * 0.05 } } else { HalfSpace { n: h.n, d: h.d - thickness } }
})
.collect();
if !hs.iter().zip(&inner).any(|((h, _), i)| i.d > h.d) {
if !hs.iter().zip(&inner).any(|((h, ..), i)| i.d > h.d) {
return Err(KernelError::Invalid("select at least one face to remove".into()));
}
let cavity = convex_polyhedron(&inner)?;
crate::ops::boolean(b, &cavity, crate::BoolOp::Cut)?.ok_or_else(|| KernelError::Failed("the shell removed everything".into()))
let (cavity, on) = polyhedron(&inner)?;
let groups: Vec<Vec<usize>> = hs.into_iter().map(|(.., under)| under).collect();
let walls = cavity_history(&cavity, &on, &groups);
let r = crate::ops::boolean(b, &cavity, crate::BoolOp::Cut)?.ok_or_else(|| KernelError::Failed("the shell removed everything".into()))?;
Ok(hollowed(r, b, walls))
}
/// `r`, `b` less a cavity whose history over `[b]` is `walls`, with its history over `[b]`.
fn hollowed(r: Body, b: &Body, walls: Option<History>) -> Body {
let h = crate::boolean_record::of_pair(&r, &crate::boolean_record::kept(b, 0, 1), &walls);
r.with_checked_history(h, &record::counts(&[b]))
}
/// The history over `[b]` of a cavity `offset` made from `b` by moving its faces (whose history
/// over `[b]` says so): each face an offset of the face it was moved from, each edge new.
fn offsets(offset: &Body) -> Option<History> {
let h = offset.history()?;
let faces: Vec<FaceProv> = h
.faces
.iter()
.map(|f| match f.origins.as_slice() {
[FaceOrigin::Face { operand, face }] => Some(record::generated(vec![FaceOrigin::Offset { operand: *operand, face: *face }])),
_ => None,
})
.collect::<Option<_>>()?;
Some(record::from_faces(&offset.solid, h.operands, faces))
}
/// Tilt the faces near `faces` by `angle` about their line on the neutral plane, so that they
@@ -172,7 +258,7 @@ pub fn draft(b: &Body, faces: &[Vec3], neutral: &Plane, pull: Vec3, angle: f64)
let size = b.size();
let mut out = Vec::new();
let mut moved = 0;
for (h, c) in &hs {
for (h, c, _) in &hs {
let chosen = faces.iter().any(|p| (h.n.dot(*p) - h.d).abs() < size * 1e-4 + 1e-6 && p.dist(*c) < size * 2.0);
if !chosen {
out.push(*h);
@@ -191,12 +277,16 @@ pub fn draft(b: &Body, faces: &[Vec3], neutral: &Plane, pull: Vec3, angle: f64)
if moved == 0 {
return Err(KernelError::Invalid("no faces to draft".into()));
}
convex_polyhedron(&out)
// Each half-space in place of the one it was made from: its faces moved (or as they were).
let (r, on) = polyhedron(&out)?;
let groups: Vec<Vec<usize>> = hs.into_iter().map(|(.., under)| under).collect();
let h = planes_history(&r, &on, &groups, b);
Ok(r.with_checked_history(h, &record::counts(&[b])))
}
/// Draft of a non-convex body: walls (planes, cylinders) next to caps square to `pull`.
fn draft_walls(b: &Body, at: &[Vec3], neutral: &Plane, pull: Vec3, angle: f64) -> Result<Body> {
let healed = Body::new(crate::heal::heal_keep(b.deep_copy(), b.size(), b.split_keep()))?;
let healed = crate::boolean_record::healed(b)?;
let size = healed.size();
let mesh = healed.tessellate((size * 1e-3).max(1e-3))?;
let mut chosen: Vec<usize> = Vec::new();
@@ -216,13 +306,22 @@ fn draft_walls(b: &Body, at: &[Vec3], neutral: &Plane, pull: Vec3, angle: f64) -
_ => return Err(KernelError::Invalid(format!("no face at {:?}", [p.x, p.y, p.z]))),
}
}
crate::offset::draft_walls(&healed, &chosen, neutral, pull, angle)
Ok(record::after(crate::offset::draft_walls(&healed, &chosen, neutral, pull, angle)?, &healed, &record::counts(&[b])))
}
/// Round every edge of a convex body with planar faces by `r`: faces shrink, each edge becomes a
/// cylinder and each corner a sphere patch (the body is the inner polyhedron grown by a ball).
/// The result's history is over `[b]`: each cylinder in place of the edges of `b` between its
/// two planes' faces, each corner patch in place of those round its corner.
pub fn round_all_edges(b: &Body, r: f64) -> Result<Body> {
let hs: Vec<HalfSpace> = planar_convex(b)?.into_iter().map(|(h, _)| h).collect();
let planes = planar_convex(b)?;
let hs: Vec<HalfSpace> = planes.iter().map(|(h, ..)| *h).collect();
let groups: Vec<Vec<usize>> = planes.iter().map(|(.., under)| under.clone()).collect();
// The edges of `b` between faces on planes f and g.
let edges = plane_edges(b, &groups);
let along = |f: usize, g: usize| -> Vec<FaceOrigin> {
edges.get(&(f.min(g), f.max(g))).into_iter().flatten().map(|&edge| FaceOrigin::Edge { operand: 0, edge }).collect()
};
let inner: Vec<HalfSpace> = hs.iter().map(|h| HalfSpace { n: h.n, d: h.d - r }).collect();
let (q, faces) = combinatorics(&inner)?;
if faces.len() != hs.len() {
@@ -269,6 +368,8 @@ pub fn round_all_edges(b: &Body, r: f64) -> Result<Body> {
if f < g { e } else { e.inverse() }
};
let mut out: Vec<mt::Face> = Vec::new();
// What each face is made from (in `out`'s order).
let mut made: Vec<FaceProv> = Vec::new();
// Planar faces.
for (hi, lp) in &faces {
let n = lp.len();
@@ -278,6 +379,7 @@ pub fn round_all_edges(b: &Body, r: f64) -> Result<Body> {
w.push(line(*hi, a, bb, &mut vert));
}
out.push(builder::try_attach_plane(&[w.into()]).map_err(|e| KernelError::Failed(format!("rounded face: {e}")))?);
made.push(on_plane(groups.get(*hi).map_or(&[], Vec::as_slice)));
}
use mt::{ParametricSurface3D, SearchNearestParameter};
let orient = |mut surf: mt::Surface, at: Vec3, outward: Vec3| -> mt::Surface {
@@ -310,6 +412,7 @@ pub fn round_all_edges(b: &Body, r: f64) -> Result<Body> {
let mid = (qa + qb) * 0.5 + out_dir * r;
let surf = orient(swept.oriented_surface(), mid, out_dir);
out.push(mt::Face::try_new(vec![w.into()], surf).map_err(|e| KernelError::Failed(format!("rounded edge: {e}")))?);
made.push(record::generated(along(f, g)));
}
// Corner sphere patches: the faces around each inner vertex, counter-clockwise.
for (qi, c) in q.iter().enumerate() {
@@ -331,10 +434,18 @@ pub fn round_all_edges(b: &Body, r: f64) -> Result<Body> {
mt::Surface::RevolutedCurve(mt::Processor::new(mt::RevolutedCurve::by_revolution(meridian.oriented_curve(), p3(*c), v3(side))));
let surf = orient(surf, *c + axis * r, axis);
out.push(mt::Face::try_new(vec![w.into()], surf).map_err(|e| KernelError::Failed(format!("rounded corner: {e}")))?);
let mut corner: Vec<FaceOrigin> =
(0..k).filter_map(|i| Some((*around.get(i)?, *around.get((i + 1) % k)?))).flat_map(|(f, g)| along(f, g)).collect();
corner.sort();
corner.dedup();
made.push(record::generated(corner));
}
let shell: mt::Shell = out.into();
let solid = Solid::try_new(vec![shell]).map_err(|e| KernelError::Failed(format!("rounded body: {e}")))?;
Body::new(solid)
let body = Body::new(solid)?;
// Every face from something of `b` (a blend between planes that met nowhere has none).
let history = made.iter().all(|f| !f.origins.is_empty()).then(|| record::from_faces(&body.solid, 1, made));
Ok(body.with_checked_history(history, &record::counts(&[b])))
})
}
@@ -345,12 +456,9 @@ fn shell_planar(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
// Face paint and authored splits are construction boundaries. Retain their
// eligibility veto through the temporary body used for the cavity Cut.
// Paint indices stay in source order by avoiding healing in that case.
let solid = if b.paint().is_some_and(|paint| !paint.faces.is_empty()) {
b.deep_copy()
} else {
crate::heal::heal_keep(b.deep_copy(), b.size(), b.split_keep())
};
let healed = Body::new(solid)?.with_paint(b.paint().cloned()).with_split_lines(b.split_lines().cloned()).inherit_regularization_protection(b);
let healed =
if b.paint().is_some_and(|paint| !paint.faces.is_empty()) { crate::boolean_record::copied(b)? } else { crate::boolean_record::healed(b)? };
let healed = healed.with_paint(b.paint().cloned()).with_split_lines(b.split_lines().cloned()).inherit_regularization_protection(b);
let size = healed.size();
let mesh = healed.tessellate((size * 1e-3).max(1e-3))?;
// Faces to open: the face of the triangle nearest each point.
@@ -422,7 +530,10 @@ fn shell_opened(b: &Body, healed: &Body, open: &[Vec3], opened: &[usize], thickn
return Ok(x);
}
match r {
Ok(Some(r)) => Ok(r),
Ok(Some(r)) => {
// Over `[healed]`, the cavity's faces offsets of the faces they line; then over `[b]`.
Ok(record::after(hollowed(r, &healed, offsets(&cavity)), &healed, &record::counts(&[b])))
}
Ok(None) => Err(KernelError::Failed("the shell removed everything".into())),
// Healing can leave faces the boolean dislikes (revolved patches): the body as it was,
// its faces to open found again.
@@ -445,15 +556,17 @@ fn shell_opened(b: &Body, healed: &Body, open: &[Vec3], opened: &[usize], thickn
}
let opened = opened_raw;
let cavity = offset_planar(b, |fi, _| if opened.contains(&fi) { margin } else { -thickness }).map_err(|_| e.clone())?;
crate::ops::boolean(b, &cavity, crate::BoolOp::Cut)
let r = crate::ops::boolean(b, &cavity, crate::BoolOp::Cut)
.map_err(|_| e)?
.ok_or_else(|| KernelError::Failed("the shell removed everything".into()))
.ok_or_else(|| KernelError::Failed("the shell removed everything".into()))?;
Ok(hollowed(r, b, offsets(&cavity)))
}
}
}
/// A shell made from the cavity with every face moved in by the thickness (open faces too),
/// joined with each open face's moved copy swept out through the body.
/// joined with each open face's moved copy swept out through the body. No history: the
/// opening's walls, swept from the cavity's edges, come from nothing of `b`.
fn shell_by_opening(b: &Body, opened: &[usize], thickness: f64, reach: f64) -> Result<Body> {
let closed = offset_planar(b, |_, _| -thickness)?;
let faces: Vec<mt::Face> = closed.solid.face_iter().cloned().collect();
@@ -472,7 +585,8 @@ fn shell_by_opening(b: &Body, opened: &[usize], thickness: f64, reach: f64) -> R
let prism = Body::new(prism)?;
cavity = crate::ops::boolean(&cavity, &prism, crate::BoolOp::Union)?.ok_or_else(|| KernelError::Failed("shell: opening".into()))?;
}
crate::ops::boolean(b, &cavity, crate::BoolOp::Cut)?.ok_or_else(|| KernelError::Failed("the shell removed everything".into()))
let r = crate::ops::boolean(b, &cavity, crate::BoolOp::Cut)?.ok_or_else(|| KernelError::Failed("the shell removed everything".into()))?;
Ok(r.without_history())
}
fn unsupported_shell(m: &str) -> KernelError {
@@ -525,7 +639,7 @@ pub fn offset_faces(b: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
if !distance.is_finite() || distance.abs() > 1e6 {
return Err(KernelError::Invalid("offset distance".into()));
}
let healed = Body::new(crate::heal::heal_keep(b.deep_copy(), b.size(), b.split_keep()))?.with_split_lines(b.splits.clone());
let healed = crate::boolean_record::healed(b)?.with_split_lines(b.splits.clone());
let size = healed.size();
let mesh = healed.tessellate((size * 1e-3).max(1e-3))?;
let mut chosen: Vec<usize> = Vec::new();
@@ -547,7 +661,7 @@ pub fn offset_faces(b: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
}
let chosen = crate::offset::with_same_surface(&healed, &chosen);
match offset_planar(&healed, |fi, _| if chosen.contains(&fi) { distance } else { 0.0 }) {
Ok(r) => Ok(r),
Ok(r) => Ok(record::after(r, &healed, &record::counts(&[b]))),
// A piece of a split face (its neighbour stays): the piece swept along its normal
// joins on (outward) or cuts in, with walls along the split lines.
Err(e) if !healed.split_keep().is_empty() => press_pieces(&healed, &chosen, distance).map_err(|_| e),
@@ -555,6 +669,8 @@ pub fn offset_faces(b: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
}
}
/// No history: the walls along the split lines are swept from edges, and come from nothing of
/// `b`.
fn press_pieces(b: &Body, chosen: &[usize], distance: f64) -> Result<Body> {
let faces: Vec<mt::Face> = b.solid.face_iter().cloned().collect();
let mut cur = b.clone();
@@ -565,5 +681,5 @@ fn press_pieces(b: &Body, chosen: &[usize], distance: f64) -> Result<Body> {
let op = if distance > 0.0 { crate::BoolOp::Union } else { crate::BoolOp::Cut };
cur = crate::ops::boolean(&cur, &tool, op)?.ok_or_else(|| KernelError::Failed("offset: the piece moved off the body".into()))?;
}
Ok(cur.with_split_lines(b.splits.clone()))
Ok(cur.with_split_lines(b.splits.clone()).without_history())
}
+198 -13
View File
@@ -13,7 +13,7 @@ use std::collections::{BTreeSet, HashMap};
use truck_modeling as mt;
use crate::Result;
use crate::body::{Body, edge_order};
use crate::body::{Body, Solid, edge_order};
use crate::history::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History};
/// A face as built, with its provenance (`None` where the construction could not be followed).
@@ -51,22 +51,23 @@ pub(crate) fn generated(origins: Vec<FaceOrigin>) -> FaceProv {
/// origins; Split when its one origin lies under other result faces too; Modified otherwise;
/// Generated with no origin.
pub(crate) fn regions(faces: Vec<(BTreeSet<FaceOrigin>, bool)>) -> Vec<FaceProv> {
let mut faces: Vec<FaceProv> = faces
.into_iter()
.map(|(origins, whole)| {
let change = match origins.len() {
0 => Change::Generated,
1 if whole => Change::Kept,
1 => Change::Modified,
_ => Change::Merged,
};
FaceProv { origins: origins.into_iter().collect(), change }
})
.collect();
let mut faces: Vec<FaceProv> = faces.into_iter().map(|(origins, whole)| region(origins, whole)).collect();
mark_shared_split(&mut faces);
faces
}
/// A result face that is a region of the input faces `origins` (see [`regions`]), before
/// faces sharing an origin are marked Split.
pub(crate) fn region(origins: BTreeSet<FaceOrigin>, whole: bool) -> FaceProv {
let change = match origins.len() {
0 => Change::Generated,
1 if whole => Change::Kept,
1 => Change::Modified,
_ => Change::Merged,
};
FaceProv { origins: origins.into_iter().collect(), change }
}
/// Faces of one origin that is under other faces too, and that still say Kept or Modified,
/// are Split (also where histories are put side by side: pieces of one body can share a
/// source).
@@ -171,6 +172,190 @@ pub(crate) fn attach(body: Body, operands: &[Operand], faces: Option<Vec<FacePro
body.with_history_counts(history, &counts)
}
/// `step`, made from `prev` (the step's one operand), with its history carried back to what
/// `prev` was made from: the operation's operands, with `counts` faces and edges. A multi-step
/// operation starts from its operand with [`crate::boolean_record::operand`] (or healed, see
/// [`crate::boolean_record::healed`]) and takes each step's result through this.
pub(crate) fn after(mut step: Body, prev: &Body, counts: &[(usize, usize)]) -> Body {
let own = step.history.take().map(std::sync::Arc::unwrap_or_clone);
let h = own.zip(prev.history()).and_then(|(h, inner)| crate::boolean_record::composed(h.substituted(0, inner)));
step.with_checked_history(h, counts)
}
/// The history over `operands` operands of a body made by building `solid`, whose faces (in its
/// face order) come from `faces`: each edge new, where its two faces meet ([`meeting`]).
pub(crate) fn from_faces(solid: &Solid, operands: usize, faces: Vec<FaceProv>) -> History {
let edges = edge_order(solid.face_iter()).1.into_iter().map(|between| EdgeProv { origins: meeting(&between, &faces), faces: between }).collect();
History { operands, faces, edges }
}
/// What a result face of a local rebuild was made from, by identity: an operand face it was
/// rebuilt from, or an operand edge it was made in place of (a blend).
#[derive(Clone, Copy, Debug)]
pub(crate) enum Was {
Face(mt::FaceID),
Edge(mt::EdgeID),
}
/// What an operation built from its operands' topology, recorded by identity while the
/// operands and the result are both alive: the faces it made (each from what [`Was`], and
/// whether it is the whole of the one operand face it was rebuilt from: the same region) and
/// the edges it made as (parts of, or joins of) operand edges. A result face or edge that is an
/// operand's own (the very same topology, kept by the building code) is that, whatever was
/// recorded for it. Every
/// other edge is new, and comes from the meeting of the two faces it separates
/// ([`Rebuild::history`]). The record holds what it names, so that no identity it was given is
/// reused by later topology while it lives.
#[derive(Debug, Default)]
pub(crate) struct Rebuild {
faces: HashMap<mt::FaceID, (mt::Face, Vec<Was>, bool)>,
edges: HashMap<mt::EdgeID, (mt::Edge, Vec<mt::EdgeID>)>,
}
impl Rebuild {
/// `new` is `from` rebuilt (trimmed differently, moved).
pub(crate) fn face(&mut self, new: &mt::Face, from: &mt::Face) {
self.made(new, vec![Was::Face(from.id())]);
}
/// `new` is `from` rebuilt over the same region (its boundary edges cut or re-made).
pub(crate) fn whole(&mut self, new: &mt::Face, from: &mt::Face) {
self.faces.insert(new.id(), (new.clone(), vec![Was::Face(from.id())], true));
}
/// `new` is made in place of the operand edges `edges` (a blend; a corner patch lists the
/// edges meeting at its corner).
pub(crate) fn blend(&mut self, new: &mt::Face, edges: impl IntoIterator<Item = mt::EdgeID>) {
self.made(new, edges.into_iter().map(Was::Edge).collect());
}
/// `new` is made from what `was` says (nothing: new to the operation).
pub(crate) fn made(&mut self, new: &mt::Face, was: Vec<Was>) {
self.faces.insert(new.id(), (new.clone(), was, false));
}
/// `new` is (a piece of, or the moved) edge `of`: an operand edge, or an edge recorded as
/// made from operand edges (a piece of a piece is a piece of the operand edge).
pub(crate) fn piece(&mut self, new: &mt::Edge, of: &mt::Edge) {
self.piece_of(new, of.id());
}
/// Every piece in `subst` (edge → the edge made in its place).
pub(crate) fn pieces<'a>(&mut self, subst: impl IntoIterator<Item = (&'a mt::EdgeID, &'a mt::Edge)>) {
for (old, new) in subst {
self.piece_of(new, *old);
}
}
fn piece_of(&mut self, new: &mt::Edge, of: mt::EdgeID) {
let roots = self.edges.get(&of).map_or_else(|| vec![of], |(_, roots)| roots.clone());
let (_, slot) = self.edges.entry(new.id()).or_insert_with(|| (new.clone(), Vec::new()));
for root in roots {
if !slot.contains(&root) {
slot.push(root);
}
}
}
/// The history of `result` (in its face order; edges by identity) made from `operands`
/// (their solids, in the operation's operand order): faces and edges that are an operand's
/// own by identity (several operands' when they share it), the rest as recorded; an edge
/// recorded nowhere separates two faces that met nowhere before, so it comes from their
/// meeting ([`meeting`]). `None` when a face is neither an operand's nor recorded, or a record
/// names topology no operand has.
pub(crate) fn history(&self, result: &Solid, operands: &[&Solid]) -> Option<(Vec<FaceProv>, HashMap<mt::EdgeID, Vec<EdgeOrigin>>)> {
let mut face_at: HashMap<mt::FaceID, Vec<FaceOrigin>> = HashMap::new();
let mut edge_at: HashMap<mt::EdgeID, Vec<(usize, usize)>> = HashMap::new();
for (operand, s) in operands.iter().enumerate() {
for (id, face) in face_index(s) {
face_at.entry(id).or_default().push(FaceOrigin::Face { operand, face });
}
for (id, edge) in edge_index(s) {
edge_at.entry(id).or_default().push((operand, edge));
}
}
let was = |w: &Was| -> Option<Vec<FaceOrigin>> {
Some(match *w {
Was::Face(id) => face_at.get(&id)?.clone(),
Was::Edge(id) => edge_at.get(&id)?.iter().map(|&(operand, edge)| FaceOrigin::Edge { operand, edge }).collect(),
})
};
let mut faces = result
.face_iter()
.map(|f| {
let (origins, whole): (BTreeSet<FaceOrigin>, bool) = match face_at.get(&f.id()) {
Some(own) => (own.iter().copied().collect(), true),
None => {
let (_, made, whole) = self.faces.get(&f.id())?;
(made.iter().map(was).collect::<Option<Vec<_>>>()?.into_iter().flatten().collect(), *whole)
}
};
let on_faces = origins.iter().filter(|o| matches!(o, FaceOrigin::Face { .. })).count();
let change = match on_faces {
0 => Change::Generated,
1 if origins.len() == 1 && whole => Change::Kept,
1 if origins.len() == 1 => Change::Modified,
_ => Change::Merged,
};
Some(FaceProv { origins: origins.into_iter().collect(), change })
})
.collect::<Option<Vec<FaceProv>>>()?;
mark_shared_split(&mut faces);
let edge_origins = |id: &mt::EdgeID| edge_at.get(id).map(|at| at.iter().map(|&(operand, edge)| EdgeOrigin::Edge { operand, edge }));
let (edges, bounds) = edge_order(result.face_iter());
let mut origins = HashMap::with_capacity(edges.len());
for (e, between) in edges.iter().zip(bounds) {
let of: Vec<EdgeOrigin> = if let Some(own) = edge_origins(&e.id()) {
own.collect()
} else if let Some((_, roots)) = self.edges.get(&e.id()) {
let mut set = BTreeSet::new();
for root in roots {
set.extend(edge_origins(root)?);
}
set.into_iter().collect()
} else {
meeting(&between, &faces)
};
origins.insert(e.id(), of);
}
Some((faces, origins))
}
}
/// The origins of a new edge between the result faces `between` (whose provenance is in
/// `faces`): the meeting of the two faces, when each comes from one origin and they differ;
/// else none (a seam, an edge between pieces of one face).
pub(crate) fn meeting(between: &[usize], faces: &[FaceProv]) -> Vec<EdgeOrigin> {
let side = |k: usize| match between.get(k).and_then(|f| faces.get(*f)).map(|p| p.origins.as_slice()) {
Some([o]) => Some(*o),
_ => None,
};
match (between.len(), side(0), side(1)) {
(2, Some(a), Some(b)) if a != b => vec![EdgeOrigin::Meet { a: a.min(b), b: a.max(b) }],
_ => Vec::new(),
}
}
/// The face and edge counts of the solids `operands`, for [`traced`].
pub(crate) fn solid_counts(operands: &[&Solid]) -> Vec<(usize, usize)> {
operands.iter().map(|s| (s.face_iter().count(), edge_order(s.face_iter()).0.len())).collect()
}
/// The body `Body::new` makes of `result`, which an operation built from `operands` (their
/// solids) as `rec` records, with that history (none where it could not be recorded).
pub(crate) fn rebuilt(result: Solid, operands: &[&Solid], rec: &Rebuild) -> Result<Body> {
let (faces, edges) = rec.history(&result, operands).unzip();
build(result, &solid_counts(operands), faces, edges.as_ref())
}
/// [`rebuilt`] for a body made as it is (a surface, which `Body::new` does not take): its
/// solid is the one the operation built.
pub(crate) fn rebuilt_body(body: Body, operands: &[&Solid], rec: &Rebuild) -> Body {
let (faces, edges) = rec.history(&body.solid, operands).unzip();
let counts = solid_counts(operands);
traced(body, &counts, faces, edges.as_ref())
}
/// Ruled faces (truck's `try_wire_homotopy` of wires `a` and `b`), each with the provenance of
/// the edges it spans: every face holds one edge of each wire, and comes from the profile
/// segments under both (`a_numbers`, `b_numbers`: by edge, in wire order).
+43 -22
View File
@@ -14,6 +14,7 @@ use truck_meshalgo::tessellation::RobustMeshableShape;
use truck_modeling as mt;
use crate::body::{Body, from_p3, p3};
use crate::record::Rebuild;
use crate::{KernelError, Result, guard};
/// What splits the faces.
@@ -457,8 +458,9 @@ fn inside(poly: &[(f64, f64)], q: (f64, f64)) -> bool {
/// Split one face (index `fi` in `faces`) by one field; `faces` is updated (the face replaced
/// by its pieces, boundary edges cut in every face).
/// Returns how many lines split it and how many faces now stand in its place (at `fi`).
fn split_one(faces: &mut Vec<mt::Face>, fi: usize, g: &Field, size: f64, new_edges: &mut Vec<mt::Edge>) -> Result<(usize, usize)> {
/// Returns how many lines split it and how many faces now stand in its place (at `fi`). The
/// pieces of cut edges are recorded in `rec`.
fn split_one(faces: &mut Vec<mt::Face>, fi: usize, g: &Field, size: f64, new_edges: &mut Vec<mt::Edge>, rec: &mut Rebuild) -> Result<(usize, usize)> {
let Some(face) = faces.get(fi).cloned() else { return Ok((0, 1)) };
let chains = trace(&face, g, size)?;
if chains.is_empty() {
@@ -659,6 +661,11 @@ fn split_one(faces: &mut Vec<mt::Face>, fi: usize, g: &Field, size: f64, new_edg
if made == 0 {
return Ok((0, 1));
}
// In the order they were cut: a piece cut again is a piece of the edge it came from.
for (c, (a, b)) in &all_cuts {
rec.piece(a, c);
rec.piece(b, c);
}
let count = pieces.len();
// Neighbours take the cut edges.
let rest: Vec<mt::Face> = faces.iter().enumerate().filter(|(k, _)| *k != fi).map(|(_, f)| recut(f, &all_cuts)).collect();
@@ -802,10 +809,14 @@ pub(crate) struct Imprint {
origin: Vec<usize>,
new_edges: Vec<mt::Edge>,
made: HashMap<usize, usize>,
/// The faces and edges made from the body's (each face over the same region as the body
/// face it came from, or a piece of it; cut edges' pieces).
rec: Rebuild,
}
fn imprint(body: &Body, faces: &[usize], fields: &[Field], size: f64) -> Result<Imprint> {
let mut out = Imprint { shells: Vec::new(), origin: Vec::new(), new_edges: Vec::new(), made: HashMap::new() };
let mut out = Imprint { shells: Vec::new(), origin: Vec::new(), new_edges: Vec::new(), made: HashMap::new(), rec: Rebuild::default() };
let body_faces: Vec<&mt::Face> = body.solid.face_iter().collect();
let mut base = 0;
for sh in body.solid.boundaries() {
let mut fs: Vec<mt::Face> = sh.face_iter().cloned().collect();
@@ -822,7 +833,7 @@ fn imprint(body: &Body, faces: &[usize], fields: &[Field], size: f64) -> Result<
continue;
}
let src = origin.get(k).copied().unwrap_or(0);
let (m, pieces) = split_one(&mut fs, k, g, size, &mut out.new_edges)?;
let (m, pieces) = split_one(&mut fs, k, g, size, &mut out.new_edges, &mut out.rec)?;
*out.made.entry(src).or_default() += m;
// The pieces stand at k..k + pieces; positions after shift.
target = target.iter().take(k).copied().chain(std::iter::repeat_n(true, pieces)).chain(target.iter().skip(k + 1).copied()).collect();
@@ -830,6 +841,11 @@ fn imprint(body: &Body, faces: &[usize], fields: &[Field], size: f64) -> Result<
k += pieces;
}
}
for (f, src) in fs.iter().zip(&origin) {
if let Some(from) = body_faces.get(*src) {
out.rec.whole(f, from);
}
}
out.shells.push(fs);
out.origin.extend(origin);
}
@@ -837,7 +853,8 @@ fn imprint(body: &Body, faces: &[usize], fields: &[Field], size: f64) -> Result<
}
/// Split the given faces (indices in `body`'s face order) where the tool meets them. The solid
/// is unchanged. A listed face the tool misses is an error naming it.
/// is unchanged. A listed face the tool misses is an error naming it. The result's history is
/// over `[body]`: each piece of a split face is Split from it, every other face Kept.
pub fn split_faces(body: &Body, faces: &[usize], tool: &SplitTool) -> Result<Body> {
split_faces_with_map(body, faces, tool).map(|(b, _)| b)
}
@@ -877,20 +894,18 @@ pub(crate) fn split_any(body: &Body, faces: &[usize], tool: &SplitTool) -> Resul
}
let shells = std::mem::take(&mut imp.shells);
let solid = mt::Solid::new_unchecked(shells.into_iter().map(mt::Shell::from).collect());
Ok((
Body {
solid: std::sync::Arc::new(solid),
coarse_missing: None,
mesh: None,
color: body.color,
paint: None,
splits: body.splits.clone(),
face_sources: None,
regularization_protected: true,
history: None,
},
imp,
))
let split = Body {
solid: std::sync::Arc::new(solid),
coarse_missing: None,
mesh: None,
color: body.color,
paint: None,
splits: body.splits.clone(),
face_sources: None,
history: None,
regularization_protected: true,
};
Ok((crate::record::rebuilt_body(split, &[&body.solid], &imp.rec), imp))
})
}
@@ -979,7 +994,8 @@ fn components(faces: Vec<mt::Face>) -> Vec<Vec<mt::Face>> {
/// Split a body into the parts on either side of a tool: a plane, or another face's surface
/// (extended). Faces are divided where the tool meets them and every part is closed with the
/// piece of the tool's surface inside the body; parts come in face order, those on the tool's
/// back first. A body the tool misses comes back whole.
/// back first. A body the tool misses comes back whole. Each part's history is over `[body]`:
/// its faces the body's (or pieces of them), the section new.
pub fn split_body(body: &Body, tool: &SplitTool) -> Result<Vec<Body>> {
body.require_brep("split body")?;
if body.solid.boundaries().len() != 1 {
@@ -1022,8 +1038,9 @@ pub fn split_body(body: &Body, tool: &SplitTool) -> Result<Vec<Body>> {
let imp = imprint(body, &all, &fields, size)?;
let new_edges = imp.new_edges;
if imp.made.values().all(|m| *m == 0) || new_edges.is_empty() {
return Ok(vec![body.clone()]);
return Ok(vec![crate::boolean_record::operand(body, 0, &crate::record::counts(&[body]))]);
}
let mut rec = imp.rec;
let faces: Vec<mt::Face> = imp.shells.into_iter().flatten().collect();
// The section: loops of the new edges on the tool surface, outer loops counter-clockwise
// about its normal and holes inside them.
@@ -1128,6 +1145,10 @@ pub fn split_body(body: &Body, tool: &SplitTool) -> Result<Vec<Body>> {
front.push(f);
}
}
// The section is new: it comes from nothing of the body.
for f in &section {
rec.made(f, Vec::new());
}
// The section closes the back side facing the tool's normal, the front side facing away.
back.extend(section.iter().cloned());
front.extend(section.iter().map(|f| f.inverse()));
@@ -1138,7 +1159,7 @@ pub fn split_body(body: &Body, tool: &SplitTool) -> Result<Vec<Body>> {
if shell.shell_condition() != mt::ShellCondition::Closed {
return Err(KernelError::Failed("a part of the split does not close".into()));
}
parts.push(Body::new(mt::Solid::new_unchecked(vec![shell]))?.with_color(body.color));
parts.push(crate::record::rebuilt(mt::Solid::new_unchecked(vec![shell]), &[&body.solid], &rec)?.with_color(body.color));
}
}
Ok(parts)
+45 -29
View File
@@ -11,6 +11,7 @@ use truck_modeling as mt;
use crate::body::{Body, from_p3, p3};
use crate::history::{Change, EdgeOrigin, FaceOrigin};
use crate::record::{Rebuild, rebuilt_body};
use crate::{KernelError, Result, guard};
impl Body {
@@ -22,7 +23,11 @@ impl Body {
/// A body of faces as they are (no orientation fix: a surface has no inside).
fn surface_body(faces: Vec<mt::Face>) -> Body {
let shell = mt::Shell::from(faces);
surface_of(mt::Shell::from(faces))
}
/// A body of a shell as it is.
fn surface_of(shell: mt::Shell) -> Body {
Body {
solid: std::sync::Arc::new(mt::Solid::new_unchecked(vec![shell])),
coarse_missing: None,
@@ -36,30 +41,23 @@ fn surface_body(faces: Vec<mt::Face>) -> Body {
}
}
/// A body from faces: a solid when they close, else a surface.
fn body_of(faces: Vec<mt::Face>) -> Result<Body> {
/// A body from faces: a solid when they close, else a surface; with the history of the
/// operation that made the faces from `operands` as `rec` records.
fn body_of(faces: Vec<mt::Face>, operands: &[&mt::Solid], rec: &Rebuild) -> Result<Body> {
let shell = mt::Shell::from(faces);
if shell.shell_condition() == mt::ShellCondition::Closed {
Body::new(mt::Solid::new_unchecked(vec![shell]))
crate::record::rebuilt(mt::Solid::new_unchecked(vec![shell]), operands, rec)
} else {
Ok(Body {
solid: std::sync::Arc::new(mt::Solid::new_unchecked(vec![shell])),
coarse_missing: None,
mesh: None,
color: None,
paint: None,
splits: None,
face_sources: None,
regularization_protected: false,
history: None,
})
Ok(rebuilt_body(surface_of(shell), operands, rec))
}
}
/// A planar surface filling a sketch region.
/// A planar surface filling a sketch region. Its history has no operands: the face is new.
pub fn patch_region(plane: &Plane, region: &Region2) -> Result<Body> {
let f = crate::build::face(plane, region)?;
Ok(surface_body(vec![f]))
let mut rec = Rebuild::default();
rec.made(&f, Vec::new());
Ok(rebuilt_body(surface_body(vec![f]), &[], &rec))
}
/// The edge of a body nearest a point (by samples along each edge).
@@ -78,7 +76,8 @@ pub(crate) fn edge_near(body: &Body, p: Vec3) -> Option<mt::Edge> {
best.filter(|(d, _)| *d <= body.size() * 0.05 + 1e-6).map(|(_, e)| e)
}
/// A planar surface filling a closed loop of a body's edges (each picked by a point on it).
/// A planar surface filling a closed loop of a body's edges (each picked by a point on it). Its
/// history is over `[body]`: the face is new, its edges the body's.
pub fn patch_edges(body: &Body, at: &[Vec3]) -> Result<Body> {
body.require_brep("patch")?;
if at.is_empty() || at.len() > 10_000 {
@@ -114,11 +113,15 @@ pub fn patch_edges(body: &Body, at: &[Vec3]) -> Result<Body> {
let wire: mt::Wire = chain.into();
let face = mt::builder::try_attach_plane(&[wire])
.map_err(|_| KernelError::Invalid("not supported yet: patching a loop that is not planar".into()))?;
Ok(surface_body(vec![face]))
// A new face on the body's own edges.
let mut rec = Rebuild::default();
rec.made(&face, Vec::new());
Ok(rebuilt_body(surface_body(vec![face]), &[&body.solid], &rec))
})
}
/// Faces of a body copied out as a surface body (faces picked by points on them).
/// Faces of a body copied out as a surface body (faces picked by points on them). Its history is
/// over `[body]`: the faces and their edges are the body's.
pub fn copy_faces(body: &Body, at: &[Vec3]) -> Result<Body> {
body.require_brep("copy faces")?;
let faces: Vec<&mt::Face> = body.solid.face_iter().collect();
@@ -130,7 +133,8 @@ pub fn copy_faces(body: &Body, at: &[Vec3]) -> Result<Body> {
chosen.push(i);
}
}
Ok(surface_body(chosen.into_iter().filter_map(|i| faces.get(i).map(|f| (*f).clone())).collect()))
let copied = surface_body(chosen.into_iter().filter_map(|i| faces.get(i).map(|f| (*f).clone())).collect());
Ok(rebuilt_body(copied, &[&body.solid], &Rebuild::default()))
}
/// Index of the face nearest a point (over its mesh triangles).
@@ -148,7 +152,8 @@ fn nearest_face(body: &Body, p: Vec3) -> Result<usize> {
}
/// Sew surface bodies along edges that coincide (within `tol`): one body, a solid when the
/// faces close up. Faces are turned so that neighbours agree.
/// faces close up. Faces are turned so that neighbours agree. The result's history is over
/// `bodies`: each face the one it was sewn from, each edge the edges sewn into it.
pub fn stitch(bodies: &[&Body], tol: f64) -> Result<Body> {
if bodies.is_empty() {
return Err(KernelError::Invalid("nothing to stitch".into()));
@@ -180,6 +185,7 @@ pub fn stitch(bodies: &[&Body], tol: f64) -> Result<Body> {
};
let mut edges: Vec<(mt::Edge, mt::Point3)> = Vec::new();
let mut out = Vec::with_capacity(faces.len());
let mut rec = Rebuild::default();
for f in &faces {
let mut wires = Vec::new();
for w in f.absolute_boundaries() {
@@ -210,6 +216,7 @@ pub fn stitch(bodies: &[&Body], tol: f64) -> Result<Body> {
x
}
};
rec.piece(&ne, &abs);
nw.push(if e.orientation() { ne } else { ne.inverse() });
}
wires.push(mt::Wire::from(nw));
@@ -218,6 +225,7 @@ pub fn stitch(bodies: &[&Body], tol: f64) -> Result<Body> {
if !f.orientation() {
nf.invert();
}
rec.whole(&nf, f);
out.push(nf);
}
// Neighbours run their shared edges opposite ways.
@@ -249,7 +257,8 @@ pub fn stitch(bodies: &[&Body], tol: f64) -> Result<Body> {
}
}
let out: Vec<mt::Face> = out.into_iter().zip(flip).map(|(f, fl)| if fl == Some(true) { f.inverse() } else { f }).collect();
body_of(out)
let operands: Vec<&mt::Solid> = bodies.iter().map(|b| &*b.solid).collect();
body_of(out, &operands, &rec)
})
}
@@ -505,7 +514,7 @@ pub fn thicken(body: &Body, thickness: f64, symmetric: bool) -> Result<Body> {
}
/// A surface body trimmed by a tool: its faces are cut where the tool meets them and the
/// pieces on the side of `keep` are kept.
/// pieces on the side of `keep` are kept. The result's history is over `[body]`.
pub fn trim(body: &Body, tool: &crate::SplitTool, keep: Vec3) -> Result<Body> {
body.require_brep("trim")?;
let total = body.solid.face_iter().count();
@@ -523,12 +532,15 @@ pub fn trim(body: &Body, tool: &crate::SplitTool, keep: Vec3) -> Result<Body> {
if kept.is_empty() {
return Err(KernelError::Invalid("nothing is left on that side of the tool".into()));
}
Ok(surface_body(kept))
// The kept pieces are the split's own; its history carries them back to `body`.
let trimmed = rebuilt_body(surface_body(kept), &[&split.solid], &Rebuild::default());
Ok(crate::record::after(trimmed, &split, &crate::record::counts(&[body])))
})
}
/// A planar surface extended: the chosen straight edges (picked by points on them) move
/// outward in the face's plane by `distance`, their neighbours lengthened to meet them.
/// outward in the face's plane by `distance`, their neighbours lengthened to meet them. The
/// result's history is over `[body]`: the face and each edge the one it was moved from.
pub fn extend(body: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
body.require_brep("extend")?;
if !(distance.is_finite() && distance > 0.0) {
@@ -577,10 +589,13 @@ pub fn extend(body: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
let p = meet(*prev, *cur).unwrap_or(cur.0);
corners.push(mt::Vertex::new(p3(p)));
}
let mut rec = Rebuild::default();
let wire: mt::Wire = (0..m)
.filter_map(|i| {
let (a, b) = (corners.get(i)?, corners.get((i + 1) % m)?);
Some(mt::Edge::new_unchecked(a, b, mt::Curve::Line(mt::Line(a.point(), b.point()))))
let (a, b, old) = (corners.get(i)?, corners.get((i + 1) % m)?, edges.get(i)?);
let e = mt::Edge::new_unchecked(a, b, mt::Curve::Line(mt::Line(a.point(), b.point())));
rec.piece(&e, old);
Some(e)
})
.collect::<Vec<_>>()
.into();
@@ -588,7 +603,8 @@ pub fn extend(body: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
if f.oriented_surface().normal(0.0, 0.0).dot(crate::body::v3(n)) < 0.0 {
f = f.inverse();
}
Ok(surface_body(vec![f]))
rec.face(&f, face);
Ok(rebuilt_body(surface_body(vec![f]), &[&body.solid], &rec))
})
}