* Add a one-surface fill of closed 3D loops to geom
A closed loop of curves that does not lie in a plane has no exact surface;
the usual answer is one smooth face fitted over it. fill_loop does that:
- the loop's best-fit plane (Newell normal), with a frame chosen from the
loop itself so the result does not depend on where the loop starts, its
direction, or where the part sits
- the loop seen along that normal must be simple and must not fold or run
along the normal; otherwise it is refused ("not supported yet"), never
filled with wrong geometry
- one non-rational bicubic B-spline height field over the loop's box,
fitted by least squares to the loop's samples (The NURBS Book 9.4.1,
9.4.3, eq. 9.62/9.65) with a thin-plate smoothness term
lambda*A*int(h_uu^2 + 2 h_uv^2 + h_vv^2), A one grid cell's area, so the
fit is the same at any scale; only planes are reproduced exactly
- span counts per direction (long narrow loops fit), refined until the
loop is within tolerance, else refused with the deviation reached
bspline: bisected span search, basis functions on a stack array, one
clamping helper shared by curves and surfaces, one binomial helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
* Patch fills non-planar loops of edges with one smooth face (#250)
surface.patch with edges refused any loop that is not planar ("not
supported yet: patching a loop that is not planar"). It now fills it:
- planar loops still get one exact planar face, but planarity is certified
conservatively: lines by their ends, B-spline/NURBS edges by their control
points, intersection curves by their samples lying on a support plane
within tolerance; a loop that can't be certified is filled instead
- other loops get one face from geom's fill_loop, bounded by the body's own
chained edges, whole (no split): its history is one generated face whose
edges are the body's, and it stitches back into a closed solid
- intersection-curve edges are sampled by a seeded, guarded double
projection shared by the planarity test and the fill (plane_fit)
- the face tolerance is 10x the planarity tolerance (documented: later
operations take the face to contain its edges within it)
- edge picking runs inside the guard; the geom->truck conversion is shared
with the T-spline sheets (spline_conv); the Newell plane fit is shared with
STEP import
Tests: 3- to 8-sided, concave (dart, arrow, plus), flat-corner, two-edge and
imported rims fill as one face and stitch closed (volumes against an
independent integral); finite mesh normals, same result for any pick order
or placement; non-height-field loops and too-fine detail are refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
* Test the Patch dialog filling a non-planar loop of picked edges
The Patch dialog's only test used a sketch profile. Picking the four
curved rim edges of a box cut by a sideways cylinder now makes one
surface.patch command whose result is a single smooth face over the rim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af0440aaab
commit
201e9d5f9f
12 files changed
+1648
-146
No files matched your search
@@ -1,5 +1,6 @@
|
||||
//! Surface bodies (open shells) and the operations that turn them into solids: Patch (a planar
|
||||
//! face filling a closed loop of edges or a sketch region), Stitch (surfaces sharing edges sewn
|
||||
//! Surface bodies (open shells) and the operations that turn them into solids: Patch (a face
|
||||
//! filling a closed loop of edges — planar, or B-spline patches for a loop that is not — or a
|
||||
//! planar sketch region), Stitch (surfaces sharing edges sewn
|
||||
//! into one body, a solid when they close), Trim (a surface cut by a tool, one side kept),
|
||||
//! Thicken (a surface offset along its normals into a solid) and copying faces out of a body.
|
||||
|
||||
@@ -76,13 +77,31 @@ 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). Its
|
||||
/// history is over `[body]`: the face is new, its edges the body's.
|
||||
/// A surface filling a closed loop of a body's edges (each picked by a point on it): one face on
|
||||
/// the body's own edges, planar when the loop is certainly planar ([`crate::plane_fit`]), else a
|
||||
/// B-spline height field over the loop's plane ([`crate::fill`]; loops that do not bound one are
|
||||
/// refused). Its history is over `[body]`: the face is new, its edges the body's own.
|
||||
pub fn patch_edges(body: &Body, at: &[Vec3]) -> Result<Body> {
|
||||
body.require_brep("patch")?;
|
||||
if at.is_empty() || at.len() > 10_000 {
|
||||
return Err(KernelError::Invalid("pick the edges of a closed loop".into()));
|
||||
}
|
||||
guard("patch", || {
|
||||
let chain = picked_loop(body, at)?;
|
||||
let Some(face) = crate::plane_fit::planar_face(&chain, crate::fill::planarity_tolerance(body))? else {
|
||||
// Not certainly planar: a fitted surface.
|
||||
return crate::fill::fill_loop(body, &chain);
|
||||
};
|
||||
// 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))
|
||||
})
|
||||
}
|
||||
|
||||
/// The edges of `body` picked by points on them, chained into one closed loop (each turned to
|
||||
/// run along it). Call it under [`guard`]: sampling an intersection curve can fail in truck.
|
||||
pub(crate) fn picked_loop(body: &Body, at: &[Vec3]) -> Result<Vec<mt::Edge>> {
|
||||
let mut edges: Vec<mt::Edge> = Vec::new();
|
||||
for p in at {
|
||||
let e = edge_near(body, *p).ok_or_else(|| KernelError::Invalid(format!("no edge at {p:?}")))?;
|
||||
@@ -90,34 +109,25 @@ pub fn patch_edges(body: &Body, at: &[Vec3]) -> Result<Body> {
|
||||
edges.push(e);
|
||||
}
|
||||
}
|
||||
guard("patch", || {
|
||||
// Chain the edges into one loop (turning them round as needed).
|
||||
let mut pool = edges;
|
||||
let first = pool.pop().ok_or_else(|| KernelError::Invalid("no edges".into()))?;
|
||||
let start = first.front().id();
|
||||
let mut at_v = first.back().clone();
|
||||
let mut chain = vec![first];
|
||||
while at_v.id() != start {
|
||||
let k = pool
|
||||
.iter()
|
||||
.position(|e| e.front().id() == at_v.id() || e.back().id() == at_v.id())
|
||||
.ok_or_else(|| KernelError::Invalid("the edges do not form a closed loop".into()))?;
|
||||
let e = pool.remove(k);
|
||||
let e = if e.front().id() == at_v.id() { e } else { e.inverse() };
|
||||
at_v = e.back().clone();
|
||||
chain.push(e);
|
||||
}
|
||||
if !pool.is_empty() {
|
||||
return Err(KernelError::Invalid("the edges form more than one loop".into()));
|
||||
}
|
||||
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()))?;
|
||||
// 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))
|
||||
})
|
||||
let mut pool = edges;
|
||||
let first = pool.pop().ok_or_else(|| KernelError::Invalid("no edges".into()))?;
|
||||
let start = first.front().id();
|
||||
let mut at_v = first.back().clone();
|
||||
let mut chain = vec![first];
|
||||
while at_v.id() != start {
|
||||
let k = pool
|
||||
.iter()
|
||||
.position(|e| e.front().id() == at_v.id() || e.back().id() == at_v.id())
|
||||
.ok_or_else(|| KernelError::Invalid("the edges do not form a closed loop".into()))?;
|
||||
let e = pool.remove(k);
|
||||
let e = if e.front().id() == at_v.id() { e } else { e.inverse() };
|
||||
at_v = e.back().clone();
|
||||
chain.push(e);
|
||||
}
|
||||
if !pool.is_empty() {
|
||||
return Err(KernelError::Invalid("the edges form more than one loop".into()));
|
||||
}
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Faces of a body copied out as a surface body (faces picked by points on them). Its history is
|
||||
|
||||
Reference in new issue
Block a user