Sweep: a parallel sweep whose path runs along the profile's plane is refused again

Kept parallel (the profile as drawn), a profile flattens to nothing where the
path turns to run along its plane. The check that refuses it (the path must
cross the plane by 5° or more all the way) lived in the stations of a
controlled sweep only; a plain parallel sweep went to SMLib unchecked and
built a degenerate solid. Both take the same check now, the plain sweep on
each segment's direction of travel at 33 points along it.

A circle profile is one closed arc, one edge in SMLib: a wall per path line,
not two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 22:19:37 -04:00
committed by Irakli Kourtis
parent b540faaf5c
commit ebb1d843ab
2 changed files with 32 additions and 8 deletions
+12
View File
@@ -531,6 +531,16 @@ mod imp {
/// it when `translate`. SMLib's sweep follows a smooth path; at a corner (the direction
/// jumping) the path is swept piece by piece ([`cornered`]).
pub(crate) fn sweep(plane: &Plane, pieces: &[Piece], path: &[crate::PathSeg], translate: bool) -> Result<Body> {
if translate {
// Every segment's direction of travel, at its ends and between them.
let bad = || KernelError::Invalid("a path segment has no direction".into());
let tangents = path
.iter()
.flat_map(|s| (0..=CROSSING_SAMPLES).map(move |i| seg_at(s, i as f64 / CROSSING_SAMPLES as f64).map(|(_, t)| t)))
.collect::<Option<Vec<Vec3>>>()
.ok_or_else(bad)?;
crate::sweep_path::crossing_check(plane, &tangents)?;
}
let runs = smooth_runs(path)?;
if runs.len() > 1 {
return cornered(plane, pieces, path, &runs, translate);
@@ -823,6 +833,8 @@ mod imp {
/// Fewest and most stations of a sweep that twists, scales or follows rails.
const MIN_STATIONS: usize = 16;
/// Directions of travel read along each path segment for the parallel sweep's crossing check.
const CROSSING_SAMPLES: usize = 32;
const MAX_STATIONS: usize = 2048;
/// The profile `region` on `plane` swept along the smooth `path` the way `ctl` asks: twisted,
+20 -8
View File
@@ -221,6 +221,21 @@ fn frames_toward(tangents: &[Vec3], d: Vec3) -> Result<Vec<[Vec3; 3]>> {
.collect()
}
/// Kept as drawn (parallel), the profile flattens to nothing where the path runs along its
/// plane: the path, by its directions of travel `tangents` (the first leaving the profile), has to
/// cross the plane at 5 degrees or more all the way.
pub(crate) fn crossing_check(plane: &Plane, tangents: &[Vec3]) -> Result<()> {
let n = plane.normal().normalized().ok_or_else(|| KernelError::Invalid("sweep frame".into()))?;
let t0 = *tangents.first().ok_or_else(|| KernelError::Invalid("sweep path".into()))?;
let axis = if n.dot(t0) < 0.0 { n * -1.0 } else { n };
if tangents.iter().any(|t| t.dot(axis) < 5f64.to_radians().sin()) {
return Err(KernelError::Invalid(
"kept parallel, the profile would lie along the sweep path: the path must keep crossing the profile's plane (by 5 deg or more)".into(),
));
}
Ok(())
}
/// Stations shaped by sweep options: each maps a world vector at the first station to its image
/// at its own (twisted and scaled about the profile's normal through the first station, then
/// turned by the orientation's frames), optionally stretched across a corner (mitre). Twist and
@@ -246,12 +261,8 @@ pub(crate) fn controlled_stations(
// Twist and scale act about the profile's normal, pointing the way the path leaves it.
let n = plane.normal().normalized().ok_or_else(|| KernelError::Invalid("sweep frame".into()))?;
let axis = if n.dot(t0) < 0.0 { n * -1.0 } else { n };
// Kept as drawn, the profile flattens to nothing where the path runs along its plane: the
// path has to cross it at 5 degrees or more all the way.
if ctl.orientation == SweepOrientation::Parallel && tangents.iter().any(|t| t.dot(axis) < 5f64.to_radians().sin()) {
return Err(KernelError::Invalid(
"kept parallel, the profile would lie along the sweep path: the path must keep crossing the profile's plane (by 5 deg or more)".into(),
));
if ctl.orientation == SweepOrientation::Parallel {
crossing_check(plane, tangents)?;
}
let mut run = Vec::with_capacity(pts.len());
let mut total = 0.0;
@@ -457,8 +468,9 @@ mod tests {
let want = std::f64::consts::PI * 4.0 * len;
assert!((v - want).abs() < 0.02 * want, "{v} vs {want}");
// The path is its lines (no wall is fitted through the points): a wall per profile
// segment (the circle's two arcs) along each of the 40 lines, and two caps.
assert_eq!(b.faces(0.01).unwrap().len(), 2 * 40 + 2, "walls along each line");
// segment (the circle is one closed arc, one edge in SMLib) along each of the 40 lines,
// and two caps.
assert_eq!(b.faces(0.01).unwrap().len(), 40 + 2, "walls along each line");
// An L-shaped path (a sharp corner) with a holed square: mitred ruled walls.
let path = [Vec3::ZERO, Vec3::new(0.0, 0.0, 20.0), Vec3::new(20.0, 0.0, 20.0)];
let plane = Plane::from_normal(Vec3::ZERO, Vec3::Z).unwrap();