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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
1 parent
d5cb276965
commit
08d7037176
19 files changed
+1146
-231
No files matched your search
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user