Sweep: orientation, twist and end scale (#1269)

Part of #771 and #772. solid.sweep and surface.sweep take orientation (perpendicular, parallel or a direction), twist (up to 4 turns) and end scale (0.01 to 100), applied about the profile's normal through the path's start; default options run the old sweep, so existing sweeps are unchanged. New fields are optional, left out when unset and noted under format 21; twist and scale are model parameters. Both sweep dialogs get Orientation, Twist and End Scale rows; labels in every catalog. Checked on main + this branch: cargo check --workspace, kernel/engine/UI sweep tests, the doc suite and the robustness goldens.
This commit is contained in:
Irakli Kourtis authored and GitHub committed 2026-10-15 06:24:31 -04:00
1 parent 0872eb959a
commit 5fa8e7d60a
30 files changed
+1120 -71

No files matched your search

+33 -9
View File
@@ -1786,6 +1786,38 @@ pub(crate) fn rotate_about(p: Vec3, center: Vec3, k: Vec3, angle: f64) -> Vec3 {
center + v * c + k.cross(v) * s + k * (k.dot(v) * (1.0 - c))
}
/// The path as a polyline: its first point, then each segment's points (arcs within 0.01 mm of
/// their chords).
pub(crate) fn path_polyline(path: &[PathSeg]) -> Result<Vec<Vec3>> {
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())),
};
let mut pts = vec![first];
for seg in path {
pts.extend(seg.points_after_start(1e-2));
}
Ok(pts)
}
/// [`sweep`] with sweep options. The default options are exactly [`sweep`]; any other
/// orientation, a twist or an end scale follow the path as a polyline through stations (see
/// [`crate::sweep_path_with`]).
pub fn sweep_with(plane: &Plane, region: &Region2, path: &[PathSeg], ctl: &crate::SweepControl) -> Result<Body> {
if ctl.is_default() {
return sweep(plane, region, path);
}
ctl.check()?;
region_ok(region)?;
if path.is_empty() || path.len() > 1000 {
return Err(KernelError::Invalid("the path needs 1…1000 segments".into()));
}
if path.iter().any(|s| s.tangents().is_none()) {
return Err(KernelError::Invalid("a path segment has no direction".into()));
}
crate::sweep_path::sweep_path_with(plane, region, &path_polyline(path)?, ctl)
}
/// Sweep a planar region (outer loop) along a chain of path segments, keeping the profile
/// perpendicular to the path (lines translate it, arcs rotate it). A path with sharp corners
/// is swept as a polyline, mitred at the corners.
@@ -1800,15 +1832,7 @@ pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body>
path.iter().map(PathSeg::tangents).collect::<Option<_>>().ok_or_else(|| KernelError::Invalid("a path segment has no direction".into()))?;
let corner = ends.windows(2).any(|w| w[0].1.dot(w[1].0) < (1e-3f64).cos());
if corner {
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())),
};
let mut pts = vec![first];
for seg in path {
pts.extend(seg.points_after_start(1e-2));
}
return crate::sweep_path::sweep_path(plane, region, &pts);
return crate::sweep_path::sweep_path(plane, region, &path_polyline(path)?);
}
// The outer loop and each hole, swept along the path; the ends are capped with holes.
let profile = region_wires(plane, &oriented(region)?)?;