A sweep along a path with sharp corners follows the corners: sweeping a rim profile along three lines (an open rectangle) gave a body of volume -0.0006 mm3 with its centre of mass at x = -3.5e9, and no error
kernel::sweep swept segment by segment: a line translates the profile along it, an arc turns it. At a sharp corner the next line runs within the plane the profile has reached, so the profile slid sideways in its own plane and the walls folded into an inside-out, zero-volume shell. Paths whose direction changes at a joint (more than 1e-3 rad) are now swept as a polyline by sweep_path, which mitres the corners; arcs become points within 0.01 mm. Smooth paths (lines and tangent arcs) are swept as before. Pipe uses the same path. Tests: sweep_round_a_sharp_corner_is_mitred sweeps a 2 x 2 square along an L of two 10 mm lines and checks the volume, 80 mm3 (fails on main). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3086c32a9a
commit
9a2a3cef8b
2 files changed
+72
-1
No files matched your search
@@ -795,13 +795,68 @@ pub enum PathSeg {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PathSeg {
|
||||||
|
/// Unit directions of the path where the segment starts and ends.
|
||||||
|
fn tangents(&self) -> Option<(Vec3, Vec3)> {
|
||||||
|
match *self {
|
||||||
|
PathSeg::Line { a, b } => {
|
||||||
|
let t = (b - a).normalized()?;
|
||||||
|
Some((t, t))
|
||||||
|
}
|
||||||
|
PathSeg::Arc { a, center, axis, angle } => {
|
||||||
|
let k = axis.normalized()?;
|
||||||
|
let end = rotate_about(a, center, k, angle);
|
||||||
|
Some((k.cross(a - center).normalized()?, k.cross(end - center).normalized()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Points along the segment (an arc within `tol` of its chords), without its start.
|
||||||
|
fn points_after_start(&self, tol: f64) -> Vec<Vec3> {
|
||||||
|
match *self {
|
||||||
|
PathSeg::Line { b, .. } => vec![b],
|
||||||
|
PathSeg::Arc { a, center, axis, angle } => {
|
||||||
|
let Some(k) = axis.normalized() else { return vec![a] };
|
||||||
|
let r = (a - center).len();
|
||||||
|
let step = if r > tol { 2.0 * (1.0 - tol / r).acos() } else { angle.abs() };
|
||||||
|
let n = ((angle.abs() / step.max(1e-6)).ceil() as usize).clamp(1, 512);
|
||||||
|
(1..=n).map(|i| rotate_about(a, center, k, angle * i as f64 / n as f64)).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `p` turned by `angle` about the unit axis `k` through `center` (right hand).
|
||||||
|
fn rotate_about(p: Vec3, center: Vec3, k: Vec3, angle: f64) -> Vec3 {
|
||||||
|
let v = p - center;
|
||||||
|
let (s, c) = angle.sin_cos();
|
||||||
|
center + v * c + k.cross(v) * s + k * (k.dot(v) * (1.0 - c))
|
||||||
|
}
|
||||||
|
|
||||||
/// Sweep a planar region (outer loop) along a chain of path segments, keeping the profile
|
/// 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).
|
/// perpendicular to the path (lines translate it, arcs rotate it). A path with sharp corners
|
||||||
|
/// is swept as a polyline, mitred at the corners.
|
||||||
pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body> {
|
pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body> {
|
||||||
region_ok(region)?;
|
region_ok(region)?;
|
||||||
if path.is_empty() || path.len() > 1000 {
|
if path.is_empty() || path.len() > 1000 {
|
||||||
return Err(KernelError::Invalid("the path needs 1…1000 segments".into()));
|
return Err(KernelError::Invalid("the path needs 1…1000 segments".into()));
|
||||||
}
|
}
|
||||||
|
// Segment by segment the profile only follows a path whose direction runs on across each
|
||||||
|
// joint: at a corner the next line would slide the profile within its own plane.
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
// The outer loop and each hole, swept along the path; the ends are capped with holes.
|
// The outer loop and each hole, swept along the path; the ends are capped with holes.
|
||||||
let mut starts = vec![wire(plane, ®ion.outer.ccw())?];
|
let mut starts = vec![wire(plane, ®ion.outer.ccw())?];
|
||||||
for h in ®ion.holes {
|
for h in ®ion.holes {
|
||||||
|
|||||||
@@ -258,6 +258,22 @@ fn loft_and_sweep() {
|
|||||||
assert!(rel(measure(&t).unwrap().volume, expect) < 1e-3, "{} vs {expect}", measure(&t).unwrap().volume);
|
assert!(rel(measure(&t).unwrap().volume, expect) < 1e-3, "{} vs {expect}", measure(&t).unwrap().volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_round_a_sharp_corner_is_mitred() {
|
||||||
|
// A 2 × 2 square centred on an L of two 10 mm lines: lines alone would slide the profile
|
||||||
|
// along the second leg within its own plane (an empty, inside-out body). Mitred, the
|
||||||
|
// volume is the area times the path length.
|
||||||
|
let sq =
|
||||||
|
Region2 { outer: Loop2::polygon(&[Vec2::new(-1.0, -1.0), Vec2::new(1.0, -1.0), Vec2::new(1.0, 1.0), Vec2::new(-1.0, 1.0)]), holes: vec![] };
|
||||||
|
let path = [
|
||||||
|
PathSeg::Line { a: Vec3::ZERO, b: Vec3::new(0.0, 0.0, 10.0) },
|
||||||
|
PathSeg::Line { a: Vec3::new(0.0, 0.0, 10.0), b: Vec3::new(10.0, 0.0, 10.0) },
|
||||||
|
];
|
||||||
|
let b = sweep(&Plane::XY, &sq, &path).unwrap();
|
||||||
|
let m = measure(&b).unwrap();
|
||||||
|
assert!(rel(m.volume, 80.0) < 1e-9, "{}", m.volume);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn moving_a_body_with_an_intersection_curve_edge_ends() {
|
fn moving_a_body_with_an_intersection_curve_edge_ends() {
|
||||||
// A cylinder cut by a tilted box keeps an elliptical intersection curve as an edge. Moving
|
// A cylinder cut by a tilted box keeps an elliptical intersection curve as an edge. Moving
|
||||||
|
|||||||
Reference in new issue
Block a user