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:
Irakli Kourtis authored and GitHub committed 2026-10-13 20:48:30 -04:00
1 parent 57d738eac4
commit aba57f1ca2
16 files changed
+480 -27

No files matched your search

+1
View File
@@ -324,6 +324,7 @@ impl FeatureKind {
| FeatureKind::Extrude { .. }
| FeatureKind::SurfaceExtrude { .. }
| FeatureKind::SurfaceRevolve { .. }
| FeatureKind::SurfaceSweep { .. }
| FeatureKind::Combine { .. }
| FeatureKind::Loft { .. }
| FeatureKind::Sweep { .. }
+1 -1
View File
@@ -55,7 +55,7 @@ impl FeatureKind {
| FeatureKind::SheetContour { sketch, .. }
| FeatureKind::Rib { sketch, .. } => sk(sketch),
FeatureKind::Patch { sketch: Some(s), .. } => sk(s),
FeatureKind::Sweep { sketch, path_sketch, .. } => {
FeatureKind::Sweep { sketch, path_sketch, .. } | FeatureKind::SurfaceSweep { sketch, path_sketch, .. } => {
sk(sketch);
sk(path_sketch);
}
+19 -4
View File
@@ -971,6 +971,17 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
symmetric: bool,
},
/// A surface body swept along a chain of sketch curves from open curve chains and the
/// boundaries of profiles: walls only, no caps.
SurfaceSweep {
sketch: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
profiles: Option<ProfileSel>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
curves: Vec<String>,
path_sketch: u64,
path: Vec<String>,
},
/// A planar surface body extended outward by `distance` past the edges through `edges`.
SurfaceExtend {
body: String,
@@ -1278,7 +1289,7 @@ impl FeatureKind {
| K::Rib { sketch, .. }
| K::SheetBase { sketch, .. }
| K::SheetContour { sketch, .. } => vec![*sketch],
K::Sweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
K::Sweep { sketch, path_sketch, .. } | K::SurfaceSweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
K::Pipe { path_sketch, .. } | K::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } => vec![*path_sketch],
K::Loft { sections, .. } => sections.iter().map(|s| s.sketch).collect(),
K::SheetFold { sketch, .. } | K::SplitFace { sketch, .. } | K::Patch { sketch, .. } => sketch.iter().copied().collect(),
@@ -1408,6 +1419,7 @@ impl FeatureKind {
FeatureKind::Patch { .. } => "PatchFeature",
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrudeFeature",
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolveFeature",
FeatureKind::SurfaceSweep { .. } => "SurfaceSweepFeature",
FeatureKind::Stitch { .. } => "StitchFeature",
FeatureKind::Thicken { .. } => "ThickenFeature",
FeatureKind::SurfaceTrim { .. } => "TrimFeature",
@@ -1478,6 +1490,7 @@ impl FeatureKind {
FeatureKind::Patch { .. } => "Patch",
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrude",
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolve",
FeatureKind::SurfaceSweep { .. } => "SurfaceSweep",
FeatureKind::Stitch { .. } => "Stitch",
FeatureKind::Thicken { .. } => "Thicken",
FeatureKind::SurfaceTrim { .. } => "Trim",
@@ -1502,7 +1515,8 @@ impl FeatureKind {
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
FeatureKind::Patch { sketch: Some(sk), profiles, .. }
| FeatureKind::SurfaceExtrude { sketch: sk, profiles: Some(profiles), .. }
| FeatureKind::SurfaceRevolve { sketch: sk, profiles: Some(profiles), .. } => {
| FeatureKind::SurfaceRevolve { sketch: sk, profiles: Some(profiles), .. }
| FeatureKind::SurfaceSweep { sketch: sk, profiles: Some(profiles), .. } => {
vec![(*sk, profiles)]
}
FeatureKind::Loft { sections, .. } => sections.iter_mut().filter(|s| s.point.is_none()).map(|s| (s.sketch, &mut s.profiles)).collect(),
@@ -1520,7 +1534,8 @@ impl FeatureKind {
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
FeatureKind::Patch { sketch: Some(sketch), profiles, .. }
| FeatureKind::SurfaceExtrude { sketch, profiles: Some(profiles), .. }
| FeatureKind::SurfaceRevolve { sketch, profiles: Some(profiles), .. } => {
| FeatureKind::SurfaceRevolve { sketch, profiles: Some(profiles), .. }
| FeatureKind::SurfaceSweep { sketch, profiles: Some(profiles), .. } => {
vec![(*sketch, profiles)]
}
FeatureKind::Loft { sections, .. } => {
@@ -1633,7 +1648,7 @@ impl FeatureKind {
},
FeatureKind::Mirror { plane, .. } => plane_exprs(plane, &mut v),
FeatureKind::Shell { thickness, .. } => v.push(thickness),
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } => {}
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } | FeatureKind::SurfaceSweep { .. } => {}
FeatureKind::Draft { angle, neutral, .. } => {
v.push(angle);
plane_exprs(neutral, &mut v);
+4 -2
View File
@@ -880,6 +880,7 @@ fn cache_key(f: &Feature, chain_fp: u64, local: u64, ctx: u64, state: &ModelStat
fn placement_digest(doc: &Document, f: &Feature) -> u64 {
let path = match &f.kind {
FeatureKind::Sweep { path_sketch, .. }
| FeatureKind::SurfaceSweep { path_sketch, .. }
| FeatureKind::Pipe { path_sketch, .. }
| FeatureKind::Pattern { pattern: crate::PatternKind::Path { path_sketch, .. }, .. } => Some(*path_sketch),
_ => None,
@@ -892,7 +893,7 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
}
if let FeatureKind::Sweep { sketch, .. } = &f.kind {
if let FeatureKind::Sweep { sketch, .. } | FeatureKind::SurfaceSweep { sketch, .. } = &f.kind {
let owner = doc.feature(*sketch).map(|feature| feature.component).unwrap_or(0);
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
}
@@ -3635,7 +3636,8 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
| FeatureKind::SurfaceExtrude { .. }
| FeatureKind::Unstitch { .. }
| FeatureKind::ReverseNormal { .. }
| FeatureKind::SurfaceRevolve { .. } => surface::eval(doc, vals, f, st),
| FeatureKind::SurfaceRevolve { .. }
| FeatureKind::SurfaceSweep { .. } => surface::eval(doc, vals, f, st),
FeatureKind::Emboss { .. }
| FeatureKind::Rib { .. }
| FeatureKind::ReplaceFace { .. }
+27 -1
View File
@@ -1,5 +1,5 @@
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend, Surface Extrude,
//! Unstitch and Reverse Normal (kernel `surfaces`).
//! Revolve and Sweep, Unstitch and Reverse Normal (kernel `surfaces`).
use super::*;
use solidcraft_geom::Seg2;
@@ -155,6 +155,32 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
}
Ok(())
}
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
let ps =
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
// As Sweep: a 3D path sketch is followed in the feature's frame, the curves with it.
let profile;
let ss = if !ps.sketch.wires.is_empty() {
profile = profile_in_frame(doc, ss, f.component)?;
&profile
} else {
ss
};
let mapped = path_in_frame(doc, ps, f.component)?;
let ps = mapped.as_ref();
let chains = sheet_chains(ss, profiles.as_ref(), curves)?;
// The path runs on from the end nearest the middle of the curves.
let pts: Vec<Vec2> = chains.iter().flatten().flat_map(|s| s.polyline(1e-2)).collect();
let start = ss.plane.to_world(pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64));
let body = match path_points(ps, path, start)? {
Some(pts) => kernel::sweep_sheet_points(&ss.plane, &chains, &pts)?,
None => kernel::sweep_sheet(&ss.plane, &chains, &path_segments(ps, path, start)?)?,
};
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::new(name, body, f.id));
Ok(())
}
_ => Err(DocError::Invalid("not a surface feature".into())),
}
}
+2
View File
@@ -118,6 +118,8 @@
//! saves as before. 0.9.0 would heal a gap the design leaves open.
//! A `SurfaceRevolve` feature (Surface Revolve: `sketch`, optional `profiles`, `curves`,
//! `axis`, `angle`, optional `angle2` and `symmetric`) is also first saved in 18.
//! A `SurfaceSweep` feature (Surface Sweep: `sketch`, optional `profiles` and `curves`,
//! `path_sketch`, `path`) is also first saved in 18.
use serde_json::Value;
+1
View File
@@ -289,6 +289,7 @@ impl FeatureKind {
| FeatureKind::BoundaryFill { .. }
| FeatureKind::Loft { .. }
| FeatureKind::Sweep { .. }
| FeatureKind::SurfaceSweep { .. }
| FeatureKind::Import { .. }
| FeatureKind::MeshImport { .. }
| FeatureKind::BodyTransfer { .. } => {}