* 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
@@ -709,6 +709,31 @@ fn surface_dialogs_make_their_commands() {
|
||||
assert!((v - 30.0 * 20.0 * 2.0).abs() < 1.0, "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_dialog_fills_a_loop_of_edges_that_is_not_planar() {
|
||||
// A box with its top cut away by a sideways cylinder: its rim is a closed loop of four
|
||||
// curved edges, not in a plane. Picking them in the Patch dialog fills it with one face.
|
||||
let mut app = app_with(json!([
|
||||
{"command": "solid.box", "params": {"length": 28, "width": 28, "height": 20, "corner": [-14, -14, 0], "body_name": "Saddle"}},
|
||||
{"command": "solid.cylinder", "params": {"radius": 40, "height": 120, "base": [0, -60, 50], "axis": "Y", "operation": "cut", "targets": ["Saddle"]}}
|
||||
]));
|
||||
let z = |x: f64| 50.0 - (1600.0 - x * x).sqrt();
|
||||
let rim = [Vec3::new(14.0, 0.0, z(14.0)), Vec3::new(0.0, 14.0, z(0.0)), Vec3::new(-14.0, 0.0, z(-14.0)), Vec3::new(0.0, -14.0, z(0.0))];
|
||||
let mut d = start(&mut app, vec![], "surface.patch");
|
||||
d.inputs[0].items = rim.iter().enumerate().map(|(i, p)| Sel::Edge { body: "Saddle".into(), index: i, point: *p }).collect();
|
||||
let cmds = apply_commands(&app, &d).unwrap();
|
||||
assert_eq!(cmds.len(), 1);
|
||||
assert_eq!(cmds[0].0, "surface.patch");
|
||||
app.session.execute(&cmds[0].0, &cmds[0].1).unwrap();
|
||||
let st = app.session.model.state();
|
||||
let fill = st.bodies.iter().find(|b| b.name != "Saddle").unwrap();
|
||||
assert!(fill.body.is_surface() && fill.body.face_count() == 1);
|
||||
// One smooth face over the rim: its area is close to the cylinder's inside the rim.
|
||||
let area = fill.mesh().measure().area;
|
||||
let want = 28.0 * 40.0 * 2.0 * (14.0f64 / 40.0).asin();
|
||||
assert!((area - want).abs() < want * 1e-2, "{area} vs {want}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_body_by_a_curved_face_sends_it_as_a_tool() {
|
||||
let mut app = app_with(json!([
|
||||
|
||||
Reference in new issue
Block a user