Surface Extrude: sheets from open curves and profile walls (#815)
surface.extrude sweeps open curve chains and profile boundaries along the sketch normal into a surface body (walls only, no caps). Saved under format 17. Refs #757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8dd583930e
commit
e540832626
17 files changed
+493
-79
No files matched your search
+59
-23
@@ -31,11 +31,41 @@ fn wire_sources(plane: &Plane, lp: &Loop2) -> Result<(mt::Wire, Vec<(mt::Edge, S
|
||||
if !lp.is_closed(1e-6) {
|
||||
return Err(KernelError::Invalid("profile loop is not closed".into()));
|
||||
}
|
||||
let verts: Vec<mt::Vertex> = lp.segs.iter().map(|s| builder::vertex(p3(plane.to_world(s.start())))).collect();
|
||||
chain_sources(plane, &lp.segs, true)
|
||||
}
|
||||
|
||||
/// An open wire in 3D through a chain of sketch segments, each starting where the one before
|
||||
/// ends (a closed chain makes a closed wire).
|
||||
pub(crate) fn chain_wire(plane: &Plane, segs: &[Seg2]) -> Result<mt::Wire> {
|
||||
let n = segs.len();
|
||||
if n == 0 || n > MAX_SEGS {
|
||||
return Err(KernelError::Invalid(format!("curve chain with {n} segments")));
|
||||
}
|
||||
if !segs.iter().all(seg_ok) {
|
||||
return Err(KernelError::Invalid("non-finite curve".into()));
|
||||
}
|
||||
for (s, t) in segs.iter().zip(segs.iter().skip(1)) {
|
||||
if s.end().dist(t.start()) > 1e-6 {
|
||||
return Err(KernelError::Invalid("the curves are not connected end to end".into()));
|
||||
}
|
||||
}
|
||||
let closed = segs.first().zip(segs.last()).is_some_and(|(f, l)| l.end().dist(f.start()) <= 1e-6);
|
||||
chain_sources(plane, segs, closed).map(|(wire, _)| wire)
|
||||
}
|
||||
|
||||
/// The edges of a chain of segments (closed: the last one ends on the first's start vertex),
|
||||
/// with each edge's segment and that segment's position in the chain (a big arc makes two
|
||||
/// edges).
|
||||
fn chain_sources(plane: &Plane, segs: &[Seg2], closed: bool) -> Result<(mt::Wire, Vec<(mt::Edge, Seg2, usize)>)> {
|
||||
let n = segs.len();
|
||||
let mut verts: Vec<mt::Vertex> = segs.iter().map(|s| builder::vertex(p3(plane.to_world(s.start())))).collect();
|
||||
if !closed && let Some(last) = segs.last() {
|
||||
verts.push(builder::vertex(p3(plane.to_world(last.end()))));
|
||||
}
|
||||
let mut edges = Vec::with_capacity(n);
|
||||
let mut sources = Vec::with_capacity(n);
|
||||
for (i, s) in lp.segs.iter().enumerate() {
|
||||
let (Some(a), Some(b)) = (verts.get(i), verts.get((i + 1) % n)) else { continue };
|
||||
for (i, s) in segs.iter().enumerate() {
|
||||
let (Some(a), Some(b)) = (verts.get(i), verts.get(if closed { (i + 1) % n } else { i + 1 })) else { continue };
|
||||
let e: mt::Edge = match *s {
|
||||
Seg2::Line { .. } => builder::line(a, b),
|
||||
Seg2::Arc { sweep, .. } => {
|
||||
@@ -128,32 +158,38 @@ fn region_ok(r: &Region2) -> Result<()> {
|
||||
if r.area().abs() < 1e-9 {
|
||||
return Err(KernelError::Invalid("profile has no area".into()));
|
||||
}
|
||||
for s in std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs) {
|
||||
let ok = match *s {
|
||||
Seg2::Line { a, b } => a.is_finite() && b.is_finite(),
|
||||
Seg2::Arc { center, radius, start, sweep } => {
|
||||
center.is_finite() && radius.is_finite() && radius > 1e-9 && start.is_finite() && sweep.is_finite()
|
||||
}
|
||||
Seg2::Cubic { p0, p1, p2, p3 } => p0.is_finite() && p1.is_finite() && p2.is_finite() && p3.is_finite(),
|
||||
Seg2::Conic { a, apex, b, w } => a.is_finite() && apex.is_finite() && b.is_finite() && w.is_finite() && w > 1e-9,
|
||||
Seg2::Bezier { n, p } => (4..=solidcraft_geom::MAX_BEZIER_DEGREE as u8).contains(&n) && p.iter().all(|q| q.is_finite()),
|
||||
};
|
||||
if !ok {
|
||||
return Err(KernelError::Invalid("non-finite profile".into()));
|
||||
}
|
||||
if !std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs).all(seg_ok) {
|
||||
return Err(KernelError::Invalid("non-finite profile".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A segment with finite, well-formed numbers.
|
||||
fn seg_ok(s: &Seg2) -> bool {
|
||||
match *s {
|
||||
Seg2::Line { a, b } => a.is_finite() && b.is_finite(),
|
||||
Seg2::Arc { center, radius, start, sweep } => {
|
||||
center.is_finite() && radius.is_finite() && radius > 1e-9 && start.is_finite() && sweep.is_finite()
|
||||
}
|
||||
Seg2::Cubic { p0, p1, p2, p3 } => p0.is_finite() && p1.is_finite() && p2.is_finite() && p3.is_finite(),
|
||||
Seg2::Conic { a, apex, b, w } => a.is_finite() && apex.is_finite() && b.is_finite() && w.is_finite() && w > 1e-9,
|
||||
Seg2::Bezier { n, p } => (4..=solidcraft_geom::MAX_BEZIER_DEGREE as u8).contains(&n) && p.iter().all(|q| q.is_finite()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A finite, orthonormal sketch frame.
|
||||
pub(crate) fn frame_ok(plane: &Plane) -> bool {
|
||||
plane.origin.is_finite()
|
||||
&& plane.x.is_finite()
|
||||
&& plane.y.is_finite()
|
||||
&& (plane.x.len2() - 1.).abs() <= 1e-8
|
||||
&& (plane.y.len2() - 1.).abs() <= 1e-8
|
||||
&& plane.x.dot(plane.y).abs() <= 1e-8
|
||||
}
|
||||
|
||||
/// Extrude each region from `start` to `end` along the plane normal (offsets in mm, `end > start`).
|
||||
pub fn extrude(plane: &Plane, regions: &[Region2], start: f64, end: f64) -> Result<Vec<Body>> {
|
||||
if !plane.origin.is_finite()
|
||||
|| !plane.x.is_finite()
|
||||
|| !plane.y.is_finite()
|
||||
|| (plane.x.len2() - 1.).abs() > 1e-8
|
||||
|| (plane.y.len2() - 1.).abs() > 1e-8
|
||||
|| plane.x.dot(plane.y).abs() > 1e-8
|
||||
{
|
||||
if !frame_ok(plane) {
|
||||
return Err(KernelError::Invalid("extrude frame is not finite and orthonormal".into()));
|
||||
}
|
||||
let (start, end) = (finite(start, "start")?, finite(end, "end")?);
|
||||
|
||||
@@ -109,7 +109,7 @@ pub use step::step_export;
|
||||
pub use step_in::{ImportedBody, StepImport, StepNode, step_import, step_import_shared, step_orientation_errors, step_validate};
|
||||
pub use step_out::{ExportBody, ExportProduct, StepHeader, step_export_bodies, step_export_products};
|
||||
pub use surface_kind::{FaceSurface, face_surface};
|
||||
pub use surfaces::{copy_faces, extend, patch_edges, patch_region, stitch, thicken, trim};
|
||||
pub use surfaces::{copy_faces, extend, extrude_sheet, patch_edges, patch_region, stitch, thicken, trim};
|
||||
pub use sweep_path::sweep_path;
|
||||
pub use topo::{CylinderFace, cylinder_face_at};
|
||||
pub use topo::{TopoCounts, merged_topology, seam_flags};
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
//! 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.
|
||||
//! Thicken (a surface offset along its normals into a solid), copying faces out of a body, and
|
||||
//! sheets swept from sketch curves (surface extrude).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mt::{BoundedCurve, InnerSpace, MetricSpace, ParametricCurve, ParametricSurface3D, SearchNearestParameter};
|
||||
use solidcraft_geom::{Plane, Region2, Vec3};
|
||||
use solidcraft_geom::{Plane, Region2, Seg2, Vec3};
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::body::{Body, from_p3, p3};
|
||||
@@ -63,6 +64,42 @@ pub fn patch_region(plane: &Plane, region: &Region2) -> Result<Body> {
|
||||
Ok(rebuilt_body(surface_body(vec![f]), &[], &rec))
|
||||
}
|
||||
|
||||
/// Most curve chains one surface extrude takes.
|
||||
const MAX_CHAINS: usize = 10_000;
|
||||
|
||||
/// A surface extruded from chains of sketch segments on `plane`: each chain (open, or closed
|
||||
/// such as a profile's boundary) moved straight from `start` to `end` along the plane normal
|
||||
/// (offsets in mm). The walls it sweeps form one surface body with no caps. Its history has no
|
||||
/// operands: every face is new.
|
||||
pub fn extrude_sheet(plane: &Plane, chains: &[Vec<Seg2>], start: f64, end: f64) -> Result<Body> {
|
||||
if !crate::build::frame_ok(plane) {
|
||||
return Err(KernelError::Invalid("extrude frame is not finite and orthonormal".into()));
|
||||
}
|
||||
if !(start.is_finite() && end.is_finite() && start.abs() < 1e7 && end.abs() < 1e7) {
|
||||
return Err(KernelError::Invalid("extrude distance is not finite".into()));
|
||||
}
|
||||
let (lo, hi) = if end >= start { (start, end) } else { (end, start) };
|
||||
if hi - lo < 1e-6 {
|
||||
return Err(KernelError::Invalid("extrude distance is zero".into()));
|
||||
}
|
||||
if chains.is_empty() || chains.len() > MAX_CHAINS {
|
||||
return Err(KernelError::Invalid("pick the curves or profiles to extrude".into()));
|
||||
}
|
||||
let base = plane.offset(lo);
|
||||
let along = crate::body::v3(plane.normal() * (hi - lo));
|
||||
let mut rec = Rebuild::default();
|
||||
let mut faces = Vec::new();
|
||||
for chain in chains {
|
||||
let wire = crate::build::chain_wire(&base, chain)?;
|
||||
let walls: mt::Shell = guard("surface extrude", || Ok(mt::builder::tsweep(&wire, along)))?;
|
||||
for f in walls.face_iter() {
|
||||
rec.made(f, Vec::new());
|
||||
faces.push(f.clone());
|
||||
}
|
||||
}
|
||||
Ok(rebuilt_body(surface_body(faces), &[], &rec))
|
||||
}
|
||||
|
||||
/// The edge of a body nearest a point (by samples along each edge).
|
||||
pub(crate) fn edge_near(body: &Body, p: Vec3) -> Option<mt::Edge> {
|
||||
let mut best: Option<(f64, mt::Edge)> = None;
|
||||
@@ -630,6 +667,49 @@ mod tests {
|
||||
(a - b).abs() / b.abs().max(1e-9)
|
||||
}
|
||||
|
||||
/// Walls swept from an open line, an open arc and a closed square: areas are length × height,
|
||||
/// no caps, and stitching two patches onto the square's walls closes the box.
|
||||
#[test]
|
||||
fn extrude_sheet_of_open_and_closed_chains() {
|
||||
let line = vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }];
|
||||
let s = extrude_sheet(&Plane::XY, std::slice::from_ref(&line), 0.0, 5.0).unwrap();
|
||||
assert!(s.is_surface() && s.face_count() == 1);
|
||||
assert!(rel(measure(&s).unwrap().area, 50.0) < 1e-9);
|
||||
// A quarter arc of radius 4, swept from -1 to 2: area r θ h.
|
||||
let arc = vec![Seg2::Arc { center: Vec2::new(0.0, 0.0), radius: 4.0, start: 0.0, sweep: std::f64::consts::FRAC_PI_2 }];
|
||||
let a = measure(&extrude_sheet(&Plane::XY, &[arc], 2.0, -1.0).unwrap()).unwrap().area;
|
||||
let want = 4.0 * std::f64::consts::FRAC_PI_2 * 3.0;
|
||||
assert!(rel(a, want) < 1e-3, "{a} vs {want}"); // (measured on meshes)
|
||||
// A line then a line back up: one chain, two faces.
|
||||
let bent = vec![line[0], Seg2::Line { a: Vec2::new(10.0, 0.0), b: Vec2::new(10.0, 6.0) }];
|
||||
assert_eq!(extrude_sheet(&Plane::XY, &[bent], 0.0, 1.0).unwrap().face_count(), 2);
|
||||
// A closed square: four walls, open at both ends; with a patch on each end, a solid box.
|
||||
let sq = Region2 {
|
||||
outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(10.0, 10.0), Vec2::new(0.0, 10.0)]),
|
||||
holes: vec![],
|
||||
};
|
||||
let walls = extrude_sheet(&Plane::XY, std::slice::from_ref(&sq.outer.segs), 0.0, 4.0).unwrap();
|
||||
assert!(walls.is_surface() && walls.face_count() == 4);
|
||||
assert!(rel(measure(&walls).unwrap().area, 160.0) < 1e-9);
|
||||
let (bottom, top) = (patch_region(&Plane::XY, &sq).unwrap(), patch_region(&Plane::XY.offset(4.0), &sq).unwrap());
|
||||
let closed = stitch(&[&walls, &bottom, &top], 1e-6).unwrap();
|
||||
assert!(!closed.is_surface());
|
||||
assert!(rel(measure(&closed).unwrap().volume, 400.0) < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extrude_sheet_refuses_bad_input() {
|
||||
let line = vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }];
|
||||
assert!(extrude_sheet(&Plane::XY, std::slice::from_ref(&line), 0.0, 0.0).is_err());
|
||||
assert!(extrude_sheet(&Plane::XY, std::slice::from_ref(&line), 0.0, f64::NAN).is_err());
|
||||
assert!(extrude_sheet(&Plane::XY, &[], 0.0, 1.0).is_err());
|
||||
assert!(extrude_sheet(&Plane::XY, &[vec![]], 0.0, 1.0).is_err());
|
||||
let gap = vec![line[0], Seg2::Line { a: Vec2::new(11.0, 0.0), b: Vec2::new(12.0, 0.0) }];
|
||||
assert!(extrude_sheet(&Plane::XY, &[gap], 0.0, 1.0).is_err());
|
||||
let nan = vec![Seg2::Line { a: Vec2::new(0.0, f64::NAN), b: Vec2::new(1.0, 0.0) }];
|
||||
assert!(extrude_sheet(&Plane::XY, &[nan], 0.0, 1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_and_thicken_a_square() {
|
||||
let r = Region2 {
|
||||
|
||||
Reference in new issue
Block a user