Surface bodies: Patch, Stitch, Trim and Thicken

Kernel (surfaces):
- Bodies with an open shell are surface bodies (Body::is_surface).
- patch_region: a planar patch of a sketch region. patch_edges: a planar
  patch filling a closed loop of a body's edges.
- copy_faces: faces copied out as a surface.
- stitch: sew surfaces along coinciding edges, turning faces to agree; the
  result is a solid when it closes.
- trim: cut a surface by a plane or face and keep one side.
- thicken: offset planar and circular-cylindrical faces along their
  normals (one side or symmetric) into a solid. Plane copies slide;
  cylinder copies scale about the axis, exact for NURBS circles. Free
  edges get ruled walls. Faces meeting at a sharp edge are refused.

Doc features Patch, Stitch, Thicken (with operations) and SurfaceTrim;
engine commands FusionSurfacePatchCommand, FusionSurfaceStitchCommand,
FusionSurfaceThickenCommand and FusionSurfaceTrimCommand.

Tests (volumes):
- A 10x10 patch thickened by 2 gives 200, also symmetric.
- Half a cylinder side thickened outward gives half a ring; both halves
  inward give the full ring.
- Six faces of a box stitched back give 6000; an open box with its top
  patched from the edges, stitched, gives 6000.
- Trim keeps 30 or 70 of 100.
- Engine: patch, trim, thicken gives 140.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 17:31:57 +00:00
1 parent feaaba156e
commit dd6c8e1a6b
11 files changed
+908 -11

No files matched your search

+28
View File
@@ -706,6 +706,34 @@ fn surface_field(s: &mt::Surface) -> Field {
Field::Surface(s.clone())
}
/// A plane or circular cylinder behind a surface (for offsetting).
pub(crate) enum Analytic {
Plane { n: Vec3 },
Cylinder { o: Vec3, axis: Vec3, r: f64, out: f64 },
}
pub(crate) fn analytic_field(s: &mt::Surface) -> Option<Analytic> {
match surface_field(s) {
Field::Plane { n, .. } => Some(Analytic::Plane { n }),
Field::Cylinder { o, axis, r, out } => Some(Analytic::Cylinder { o, axis, r, out }),
_ => None,
}
}
/// Which side of a tool a point is on.
pub(crate) fn side_of_point(tool: &SplitTool, p: Vec3) -> Result<f64> {
let f = fields(tool)?;
let [g] = f.as_slice() else { return Err(KernelError::Invalid("one tool at a time".into())) };
Ok(g.eval(p))
}
/// Which side of a tool a face lies on (its field well inside it).
pub(crate) fn face_side(face: &mt::Face, tool: &SplitTool, size: f64) -> Result<f64> {
let f = fields(tool)?;
let [g] = f.as_slice() else { return Err(KernelError::Invalid("one tool at a time".into())) };
side_of(face, g, size)
}
fn fields(tool: &SplitTool) -> Result<Vec<Field>> {
Ok(match tool {
SplitTool::Plane(p) => vec![Field::Plane { o: p.origin, n: p.normal() }],