Tapered extrudes accept profiles that mix lines and tangent arcs (slots, D-profiles, rounded rectangles): each arc keeps its centre and angles and its radius moves with the taper, so its side stays an exact cone; lines are offset and mitred. An arc meeting another edge at a corner is still refused. Un-ignores the #546 regression test. Fixes #546. Co-authored-by: jonaswre <[email protected]>
This commit is contained in:
1 parent
651e8e0853
commit
abff7a49bb
3 files changed
+106
-3
No files matched your search
@@ -2923,7 +2923,6 @@ fn a_pattern_of_a_reduced_fillet_rounds_each_copy_alike() {
|
||||
/// half-round front R 4.6 tangent to them) grows by d = z · tan 5° with mitred corners:
|
||||
/// A(d) = A₀ + P·d + (2 + π/2)·d², integrated over the 10 mm height.
|
||||
#[test]
|
||||
#[ignore = "fails until #546 is fixed"]
|
||||
fn tapered_extrude_of_a_profile_mixing_lines_and_an_arc() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "D"}));
|
||||
|
||||
@@ -494,7 +494,8 @@ pub fn torus(center: Vec3, major: f64, minor: f64) -> Result<Body> {
|
||||
}
|
||||
|
||||
/// Offset every segment of a loop to its right by `d` (outward for a region's outer loop and
|
||||
/// holes alike). Loops of lines (mitred corners) and loops of arcs on one circle are supported.
|
||||
/// holes alike). Loops of lines (mitred corners), loops of arcs on one circle, and loops of lines
|
||||
/// and arcs that meet the arcs tangentially are supported.
|
||||
fn offset_loop(lp: &Loop2, d: f64) -> Result<Loop2> {
|
||||
if d == 0.0 {
|
||||
return Ok(lp.clone());
|
||||
@@ -518,6 +519,9 @@ fn offset_loop(lp: &Loop2, d: f64) -> Result<Loop2> {
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
return Ok(Loop2 { segs });
|
||||
}
|
||||
if lp.segs.iter().any(|s| matches!(s, Seg2::Arc { .. })) {
|
||||
return offset_lines_and_arcs(lp, d);
|
||||
}
|
||||
let n = lp.segs.len();
|
||||
let mut lines: Vec<(Vec2, Vec2)> = Vec::with_capacity(n);
|
||||
for s in &lp.segs {
|
||||
@@ -544,6 +548,75 @@ fn offset_loop(lp: &Loop2, d: f64) -> Result<Loop2> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// [`offset_loop`] for a loop of lines and arcs. Each arc keeps its centre and angles and its
|
||||
/// radius moves by `d` (so its side stays an exact cone), each line moves `d` to its right, and
|
||||
/// two lines meeting at a corner are mitred. An arc has to meet its neighbours tangentially: at
|
||||
/// a corner its side would meet the next one along a conic, not a straight ruling.
|
||||
fn offset_lines_and_arcs(lp: &Loop2, d: f64) -> Result<Loop2> {
|
||||
let closes = || KernelError::Invalid("the taper closes the profile".into());
|
||||
let right = |v: Vec2| Vec2::new(v.y, -v.x);
|
||||
let n = lp.segs.len();
|
||||
let moved: Vec<Option<Seg2>> = lp
|
||||
.segs
|
||||
.iter()
|
||||
.map(|s| match *s {
|
||||
Seg2::Line { .. } => Ok(None),
|
||||
Seg2::Arc { center, radius, start, sweep } => {
|
||||
let r = radius + sweep.signum() * d;
|
||||
if r > 1e-6 { Ok(Some(Seg2::Arc { center, radius: r, start, sweep })) } else { Err(closes()) }
|
||||
}
|
||||
_ => Err(KernelError::Failed("not supported yet: tapered profiles with splines or conics".into())),
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
// Where each segment starts after the offset.
|
||||
let mut joints = Vec::with_capacity(n);
|
||||
for (i, s) in lp.segs.iter().enumerate() {
|
||||
let k = (i + n - 1) % n;
|
||||
let (Some(prev), Some(prev_moved), Some(this_moved)) = (lp.segs.get(k), moved.get(k), moved.get(i)) else {
|
||||
return Err(KernelError::Failed("taper: profile loop".into()));
|
||||
};
|
||||
let (tin, tout) = (prev.end_tangent(), s.start_tangent());
|
||||
let tangent = tin.cross(tout).abs() < 1e-6 && tin.dot(tout) > 0.0;
|
||||
joints.push(match (prev_moved, this_moved) {
|
||||
(Some(a), _) if tangent => a.end(),
|
||||
(None, Some(b)) if tangent => b.start(),
|
||||
(None, None) => {
|
||||
let v = s.start();
|
||||
let (p0, p1) = (v + right(tin) * d, v + right(tout) * d);
|
||||
let den = tin.cross(tout);
|
||||
if den.abs() < 1e-12 { p1 } else { p0 + tin * ((p1 - p0).cross(tout) / den) }
|
||||
}
|
||||
_ => return Err(KernelError::Failed("not supported yet: tapered profiles where an arc meets another edge at a corner".into())),
|
||||
});
|
||||
}
|
||||
let mut segs = Vec::with_capacity(n);
|
||||
for (i, s) in lp.segs.iter().enumerate() {
|
||||
let (Some(&a), Some(&b), Some(m)) = (joints.get(i), joints.get((i + 1) % n), moved.get(i)) else {
|
||||
return Err(KernelError::Failed("taper: profile loop".into()));
|
||||
};
|
||||
match m {
|
||||
Some(arc) => {
|
||||
if arc.start().dist(a) > 1e-6 || arc.end().dist(b) > 1e-6 {
|
||||
return Err(KernelError::Failed("not supported yet: tapered profiles where an arc meets another edge at a corner".into()));
|
||||
}
|
||||
segs.push(*arc);
|
||||
}
|
||||
None => {
|
||||
// A line that turned around has been swallowed by its neighbours.
|
||||
if (b - a).dot(s.end() - s.start()) <= 0.0 {
|
||||
return Err(closes());
|
||||
}
|
||||
segs.push(Seg2::Line { a, b });
|
||||
}
|
||||
}
|
||||
}
|
||||
let out = Loop2 { segs };
|
||||
if out.signed_area().signum() != lp.signed_area().signum() || out.signed_area().abs() < 1e-9 {
|
||||
return Err(closes());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Extrude one region by `length` along `dir_sign` × the plane normal with a taper angle
|
||||
/// (radians; positive grows the profile).
|
||||
pub fn extrude_tapered(plane: &Plane, region: &Region2, length: f64, dir_sign: f64, taper: f64) -> Result<Body> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -429,6 +429,37 @@ fn tapered_extrudes() {
|
||||
assert!(extrude_tapered(&Plane::XY, &c, 10.0, 1.0, -60f64.to_radians()).is_err(), "closes the profile");
|
||||
}
|
||||
|
||||
/// A slot (two lines and two half-circles, tangent) tapered inward: every edge moves in by
|
||||
/// d = z · tan 8°, so A(d) = A₀ − P·d + π·d² (#546). An arc meeting a line at a corner is refused.
|
||||
#[test]
|
||||
fn tapered_extrude_of_lines_and_arcs() {
|
||||
let (r, l, h, a) = (5.0, 20.0, 12.0, 8f64.to_radians());
|
||||
let slot = Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Line { a: Vec2::new(0.0, -r), b: Vec2::new(l, -r) },
|
||||
Seg2::Arc { center: Vec2::new(l, 0.0), radius: r, start: -PI / 2.0, sweep: PI },
|
||||
Seg2::Line { a: Vec2::new(l, r), b: Vec2::new(0.0, r) },
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: r, start: PI / 2.0, sweep: PI },
|
||||
],
|
||||
};
|
||||
let b = extrude_tapered(&Plane::XY, &Region2 { outer: slot, holes: vec![] }, h, 1.0, -a).unwrap();
|
||||
let (a0, p, t) = (2.0 * r * l + PI * r * r, 2.0 * l + 2.0 * PI * r, a.tan());
|
||||
let v = a0 * h - p * t * h * h / 2.0 + PI * t * t * h.powi(3) / 3.0;
|
||||
let m = measure(&b).unwrap();
|
||||
assert!(rel(m.volume, v) < 5e-4, "{} vs {v}", m.volume);
|
||||
assert_eq!(m.merged.faces, 6);
|
||||
// A quarter disc: the arc meets both lines at right angles.
|
||||
let quarter = Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Line { a: Vec2::ZERO, b: Vec2::new(r, 0.0) },
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: r, start: 0.0, sweep: PI / 2.0 },
|
||||
Seg2::Line { a: Vec2::new(0.0, r), b: Vec2::ZERO },
|
||||
],
|
||||
};
|
||||
let e = extrude_tapered(&Plane::XY, &Region2 { outer: quarter, holes: vec![] }, h, 1.0, a).unwrap_err();
|
||||
assert!(e.to_string().contains("arc meets another edge at a corner"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polyhedra_shell_and_draft() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(50.0, 40.0, 30.0)).unwrap();
|
||||
|
||||
Reference in new issue
Block a user