Surface Sweep: sheets carried from open curves and profiles along a path (#843)
Surface Sweep carries open curves and closed profile boundaries along a sketch path into surface bodies, with smooth walls along curved point paths. Adds the SurfaceSweep feature (format 18 note), the surface.sweep command, a dialog with Edit Feature support, and kernel and engine tests. Part of #757.
This commit is contained in:
1 parent
57d738eac4
commit
aba57f1ca2
16 files changed
+480
-27
No files matched your search
@@ -3,7 +3,7 @@
|
||||
//! 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), copying faces out of a body, and
|
||||
//! sheets swept from sketch curves (surface extrude and revolve).
|
||||
//! sheets swept from sketch curves (surface extrude, revolve and sweep).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -162,6 +162,120 @@ pub fn revolve_sheet(plane: &Plane, chains: &[Vec<Seg2>], axis_origin: Vec2, axi
|
||||
body_of(faces, &[], &rec)
|
||||
}
|
||||
|
||||
/// A surface swept from chains of sketch segments on `plane` along a path that starts near
|
||||
/// them. Along a path whose direction runs on across every joint, lines slide the chains and
|
||||
/// arcs turn them, exactly; any other path (curves followed as polylines) goes through
|
||||
/// [`sweep_sheet_points`]. The walls form one surface body with no caps (a solid where they
|
||||
/// close); its history has no operands: every face is new.
|
||||
pub fn sweep_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[crate::PathSeg]) -> Result<Body> {
|
||||
use crate::PathSeg;
|
||||
if !crate::build::frame_ok(plane) {
|
||||
return Err(KernelError::Invalid("sweep frame is not finite and orthonormal".into()));
|
||||
}
|
||||
if chains.is_empty() || chains.len() > MAX_CHAINS {
|
||||
return Err(KernelError::Invalid("pick the curves or profiles to sweep".into()));
|
||||
}
|
||||
if path.is_empty() || path.len() > 1000 {
|
||||
return Err(KernelError::Invalid("the path needs 1…1000 segments".into()));
|
||||
}
|
||||
let ends: Vec<(Vec3, Vec3)> =
|
||||
path.iter().map(PathSeg::tangents).collect::<Option<_>>().ok_or_else(|| KernelError::Invalid("a path segment has no direction".into()))?;
|
||||
let first = match path.first() {
|
||||
Some(PathSeg::Line { a, .. } | PathSeg::Arc { a, .. }) => *a,
|
||||
None => return Err(KernelError::Invalid("the path needs 1…1000 segments".into())),
|
||||
};
|
||||
if ends.windows(2).any(|w| w[0].1.dot(w[1].0) < (1e-3f64).cos()) {
|
||||
let mut pts = vec![first];
|
||||
for seg in path {
|
||||
pts.extend(seg.points_after_start(1e-2));
|
||||
}
|
||||
return sweep_sheet_points(plane, chains, &pts);
|
||||
}
|
||||
let mut rec = Rebuild::default();
|
||||
let mut faces = Vec::new();
|
||||
for chain in chains {
|
||||
let mut cur = crate::build::chain_wire(plane, chain)?;
|
||||
for seg in path {
|
||||
let (part, map): (mt::Shell, Box<dyn Fn(Vec3) -> Vec3>) = match *seg {
|
||||
PathSeg::Line { a, b } => {
|
||||
(guard("surface sweep", || Ok(mt::builder::tsweep(&cur, crate::body::v3(b - a))))?, Box::new(move |p| p + (b - a)))
|
||||
}
|
||||
PathSeg::Arc { center, axis, angle, .. } => {
|
||||
let k = axis.normalized().ok_or_else(|| KernelError::Invalid("a path arc has no axis".into()))?;
|
||||
let part = guard("surface sweep", || Ok(mt::builder::rsweep(&cur, p3(center), crate::body::v3(k), mt::Rad(angle))))?;
|
||||
(part, Box::new(move |p| crate::build::rotate_about(p, center, k, angle)))
|
||||
}
|
||||
};
|
||||
let next = far_wire(&part, &cur, map.as_ref())?;
|
||||
for f in part.face_iter() {
|
||||
rec.made(f, Vec::new());
|
||||
faces.push(f.clone());
|
||||
}
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
body_of(faces, &[], &rec)
|
||||
}
|
||||
|
||||
/// A surface swept from chains of sketch segments on `plane` along a 3D polyline path: one smooth
|
||||
/// B-spline wall per segment where the path has no corners, else ruled walls between the path's
|
||||
/// points, mitred at the corners (see [`sweep_sheet`]).
|
||||
pub fn sweep_sheet_points(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Body> {
|
||||
if !crate::build::frame_ok(plane) {
|
||||
return Err(KernelError::Invalid("sweep frame is not finite and orthonormal".into()));
|
||||
}
|
||||
if chains.is_empty() || chains.len() > MAX_CHAINS {
|
||||
return Err(KernelError::Invalid("pick the curves or profiles to sweep".into()));
|
||||
}
|
||||
let sheet = |faces: Vec<mt::Face>| {
|
||||
let mut rec = Rebuild::default();
|
||||
for f in &faces {
|
||||
rec.made(f, Vec::new());
|
||||
}
|
||||
body_of(faces, &[], &rec)
|
||||
};
|
||||
let ruled = sheet(crate::sweep_path::ruled_sheet(plane, chains, path)?)?;
|
||||
// Smooth walls where the path has no corners, kept when their area agrees with the ruled
|
||||
// walls' (a fit that went wrong falls back to those).
|
||||
if let Ok(Some(faces)) = crate::sweep_path::smooth_sheet(plane, chains, path)
|
||||
&& let Ok(smooth) = sheet(faces)
|
||||
&& let (Ok(a), Ok(b)) = (crate::measure(&smooth), crate::measure(&ruled))
|
||||
&& (a.area - b.area).abs() <= 0.02 * b.area.abs()
|
||||
{
|
||||
return Ok(smooth);
|
||||
}
|
||||
Ok(ruled)
|
||||
}
|
||||
|
||||
/// The far side of a sweep of the wire `start`: for each of its edges, in order and the same
|
||||
/// way round, the edge of `swept` that `map` carries it onto (matched by its ends and middle).
|
||||
fn far_wire(swept: &mt::Shell, start: &mt::Wire, map: &dyn Fn(Vec3) -> Vec3) -> Result<mt::Wire> {
|
||||
let near = |p: Vec3, q: Vec3| p.dist(q) <= 1e-6 * (1.0 + p.len().max(q.len()));
|
||||
let mid = |e: &mt::Edge| {
|
||||
let c = e.oriented_curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
from_p3(c.subs((t0 + t1) / 2.0))
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for e in start.edge_iter() {
|
||||
let (a, b, m) = (map(from_p3(e.front().point())), map(from_p3(e.back().point())), map(mid(e)));
|
||||
let found = swept.edge_iter().find_map(|f| {
|
||||
let (fa, fb) = (from_p3(f.front().point()), from_p3(f.back().point()));
|
||||
if !near(mid(&f), m) {
|
||||
None
|
||||
} else if near(fa, a) && near(fb, b) {
|
||||
Some(f)
|
||||
} else if near(fa, b) && near(fb, a) {
|
||||
Some(f.inverse())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
out.push(found.ok_or_else(|| KernelError::Failed("sweep: lost the curves along the path".into()))?);
|
||||
}
|
||||
Ok(out.into())
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@@ -879,6 +993,66 @@ mod tests {
|
||||
assert!(revolve_sheet(&Plane::XY, &[vec![line((1.0, 0.0), (1.0, 1.0))]], Vec2::new(0.0, 0.0), Vec2::new(0.0, 0.0), 0.0, 1.0).is_err());
|
||||
}
|
||||
|
||||
/// A line along x swept up z: straight (length × height), then on round a quarter turn
|
||||
/// (a quarter cylinder of radius 5 about an axis along x: 10 × 5π/2, the arc taking up the
|
||||
/// line's far edge), and along a path with a corner (mitred ruled walls, 10 × both legs); a
|
||||
/// closed square's four walls; a 3D point path.
|
||||
#[test]
|
||||
fn sweep_sheet_areas() {
|
||||
use crate::PathSeg;
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
let line = vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }];
|
||||
let up = PathSeg::Line { a: Vec3::ZERO, b: Vec3::new(0.0, 0.0, 5.0) };
|
||||
let area = |b: &Body| measure(b).unwrap().area;
|
||||
let straight = sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[up]).unwrap();
|
||||
assert!(straight.is_surface() && rel(area(&straight), 50.0) < 1e-9);
|
||||
let bend = PathSeg::Arc { a: Vec3::new(0.0, 0.0, 5.0), center: Vec3::new(0.0, 5.0, 5.0), axis: -Vec3::X, angle: FRAC_PI_2 };
|
||||
let smooth = sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[up, bend]).unwrap();
|
||||
assert!(smooth.is_surface());
|
||||
let want = 50.0 + 10.0 * 5.0 * FRAC_PI_2;
|
||||
assert!(rel(area(&smooth), want) < 2e-3, "{} vs {want}", area(&smooth)); // (measured on meshes)
|
||||
let over = PathSeg::Line { a: Vec3::new(0.0, 0.0, 5.0), b: Vec3::new(0.0, 5.0, 5.0) };
|
||||
let cornered = sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[up, over]).unwrap();
|
||||
assert!(cornered.is_surface() && rel(area(&cornered), 100.0) < 1e-9, "{}", area(&cornered));
|
||||
let sq = Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0), Vec2::new(4.0, 4.0), Vec2::new(0.0, 4.0)]);
|
||||
let walls = sweep_sheet(&Plane::XY, std::slice::from_ref(&sq.segs), &[up]).unwrap();
|
||||
assert!(walls.is_surface() && walls.face_count() == 4 && rel(area(&walls), 80.0) < 1e-9);
|
||||
let pts: Vec<Vec3> = (0..=10).map(|i| Vec3::new(0.0, 0.0, i as f64)).collect();
|
||||
assert!(rel(area(&sweep_sheet_points(&Plane::XY, std::slice::from_ref(&line), &pts).unwrap()), 100.0) < 1e-6);
|
||||
// A smooth point path: a quarter circle of radius 30 in the YZ plane. One smooth wall (not
|
||||
// forty ruled strips), a piece of cylinder: 10 × 15π.
|
||||
let arc: Vec<Vec3> = (0..=40)
|
||||
.map(|i| {
|
||||
let t = i as f64 / 40.0 * FRAC_PI_2;
|
||||
Vec3::new(0.0, 30.0 * (1.0 - t.cos()), 30.0 * t.sin())
|
||||
})
|
||||
.collect();
|
||||
let curved = sweep_sheet_points(&Plane::XY, std::slice::from_ref(&line), &arc).unwrap();
|
||||
assert_eq!(curved.face_count(), 1, "smooth wall");
|
||||
let want = 10.0 * 30.0 * FRAC_PI_2;
|
||||
assert!(rel(area(&curved), want) < 2e-3, "{} vs {want}", area(&curved)); // (measured on meshes)
|
||||
// A bent chain along the same path: two smooth walls joined by one shared seam edge.
|
||||
let bent = vec![line[0], Seg2::Line { a: Vec2::new(10.0, 0.0), b: Vec2::new(10.0, 5.0) }];
|
||||
let two = sweep_sheet_points(&Plane::XY, &[bent], &arc).unwrap();
|
||||
assert_eq!(two.face_count(), 2);
|
||||
let edges: std::collections::HashSet<_> = two.solid.edge_iter().map(|e| e.id()).collect();
|
||||
assert_eq!(edges.len(), 7, "the walls share their seam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sweep_sheet_refuses_bad_input() {
|
||||
use crate::PathSeg;
|
||||
let line = vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }];
|
||||
let up = PathSeg::Line { a: Vec3::ZERO, b: Vec3::new(0.0, 0.0, 5.0) };
|
||||
assert!(sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[]).is_err());
|
||||
assert!(sweep_sheet(&Plane::XY, &[], &[up]).is_err());
|
||||
assert!(sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[PathSeg::Line { a: Vec3::ZERO, b: Vec3::ZERO }]).is_err());
|
||||
let no_axis = PathSeg::Arc { a: Vec3::ZERO, center: Vec3::new(0.0, 5.0, 0.0), axis: Vec3::ZERO, angle: 1.0 };
|
||||
assert!(sweep_sheet(&Plane::XY, std::slice::from_ref(&line), &[no_axis]).is_err());
|
||||
assert!(sweep_sheet_points(&Plane::XY, std::slice::from_ref(&line), &[Vec3::ZERO]).is_err());
|
||||
assert!(sweep_sheet_points(&Plane::XY, std::slice::from_ref(&line), &[Vec3::ZERO, Vec3::new(f64::NAN, 0.0, 0.0)]).is_err());
|
||||
}
|
||||
|
||||
#[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) }];
|
||||
|
||||
Reference in new issue
Block a user