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
|
||||
/// 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> {
|
||||
region_ok(region)?;
|
||||
if path.is_empty() || path.len() > 1000 {
|
||||
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.
|
||||
let mut starts = vec![wire(plane, ®ion.outer.ccw())?];
|
||||
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);
|
||||
}
|
||||
|
||||
#[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]
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user