mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user