Fillets along a straight edge between two cylinders: trim curved cap edges to the contacts (#238)
* Fillets along a straight edge between two cylinders: the rolling-ball blend trims curved cap edges to its contacts The edge where two overlapping extruded arcs meet is a straight line between two cylinders ending on planar caps. The rolling-ball blend built its sections and surface, but only straight seams could be shortened to the contacts, so a circular cap edge was refused with "a curved edge where the blend ends". Curved seams now keep the part of their curve between the new ends, found from the nearest of a few samples (a search from the far end of a long arc can settle on the opposite side of the circle). The ball's surface projections start from each face's last answer instead of a cold search, falling back to one when the warm answer leaves the surface's parameters or is farther than the last point: about 4.5x faster per fillet (370 -> 80 ms), so dragging the radius is usable. Fixes #237 Co-Authored-By: Claude Opus 5.5 <[email protected]> * Curve blend: refuse a closed curved seam where the blend ends A seam that starts and ends at the same vertex (a full circle) has no "between" two contacts, so trimming it built a broken edge. Return "not supported yet" instead. The curved-seam trim moves into trim_curved_seam so it can be tested directly. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: bherbruck <[email protected]>
This commit is contained in:
4 files changed
+255
-5
No files matched your search
+2
-1
@@ -160,7 +160,8 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
||||
convex planar body or of an extruded part; fillets inside patterns; closed chains of curved
|
||||
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
|
||||
blend a rational B-spline through exact arcs), closed or ending at planar faces square to the
|
||||
edges.
|
||||
edges, where straight or curved cap edges are trimmed to the contacts (the straight edge
|
||||
between two overlapping extruded arcs).
|
||||
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
|
||||
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
|
||||
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
|
||||
|
||||
@@ -123,6 +123,69 @@ mod mirror_planes;
|
||||
mod primitive_points;
|
||||
mod robustness;
|
||||
|
||||
/// Plan area a round of radius `r` fills at the concave corner `x` where circles (c1, r1) and
|
||||
/// (c2, r2) cross: the triangle of its contacts and the corner, less the three circular segments.
|
||||
fn round_gap(c1: [f64; 2], r1: f64, c2: [f64; 2], r2: f64, x: [f64; 2], r: f64) -> f64 {
|
||||
let (a, b) = (r1 + r, r2 + r);
|
||||
let d = (c2[0] - c1[0]).hypot(c2[1] - c1[1]);
|
||||
let u = [(c2[0] - c1[0]) / d, (c2[1] - c1[1]) / d];
|
||||
let along = (a * a - b * b + d * d) / (2.0 * d);
|
||||
let side = if (x[0] - c1[0]) * -u[1] + (x[1] - c1[1]) * u[0] < 0.0 { -1.0 } else { 1.0 };
|
||||
let off = (a * a - along * along).sqrt() * side;
|
||||
let f = [c1[0] + u[0] * along - u[1] * off, c1[1] + u[1] * along + u[0] * off];
|
||||
let t1 = [c1[0] + (f[0] - c1[0]) * r1 / a, c1[1] + (f[1] - c1[1]) * r1 / a];
|
||||
let t2 = [c2[0] + (f[0] - c2[0]) * r2 / b, c2[1] + (f[1] - c2[1]) * r2 / b];
|
||||
let angle = |c: [f64; 2], p: [f64; 2], q: [f64; 2]| {
|
||||
let (v, w) = ([p[0] - c[0], p[1] - c[1]], [q[0] - c[0], q[1] - c[1]]);
|
||||
((v[0] * w[0] + v[1] * w[1]) / (v[0].hypot(v[1]) * w[0].hypot(w[1]))).clamp(-1.0, 1.0).acos()
|
||||
};
|
||||
let segment = |r: f64, t: f64| r * r * (t - t.sin()) / 2.0;
|
||||
let triangle = ((x[0] - t1[0]) * (t2[1] - t1[1]) - (t2[0] - t1[0]) * (x[1] - t1[1])).abs() / 2.0;
|
||||
triangle - segment(r1, angle(c1, t1, x)) - segment(r2, angle(c2, x, t2)) - segment(r, angle(f, t2, t1))
|
||||
}
|
||||
|
||||
/// A profile of two overlapping arcs, extruded: the vertical edges where the two walls meet are
|
||||
/// straight lines between cylinders, and their rounds end on the caps across curved edges.
|
||||
#[test]
|
||||
fn fillet_the_edges_between_two_overlapping_cylinders() {
|
||||
let (c1, r1) = ([0.0, 0.0], 17.140595088852663);
|
||||
let (c2, r2) = ([10.715766135516247, 13.378055020402751], 9.666944358807896);
|
||||
let (p, q) = ([1.7728134670876865, 17.048669520256194], [16.250324167975673, 5.452243981674495]);
|
||||
let profile = 1087.5018766864137;
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": c1, "start": p, "end": q}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": c2, "start": q, "end": p}));
|
||||
let si = run(&mut s, "sketch.inspect", json!({}));
|
||||
assert!(rel(si["profiles"][0]["area_mm2"].as_f64().unwrap(), profile) < 1e-9, "{si}");
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 10}));
|
||||
let counts = |s: &mut Session| {
|
||||
let m = run(s, "inspect.measure", json!({}));
|
||||
(m["total"]["faces"].as_u64(), m["total"]["edges"].as_u64(), m["total"]["vertices"].as_u64())
|
||||
};
|
||||
assert_eq!(counts(&mut s), (Some(4), Some(6), Some(4)));
|
||||
let (gp, gq) = (round_gap(c1, r1, c2, r2, p, 6.0), round_gap(c1, r1, c2, r2, q, 6.0));
|
||||
// One round: a blend face; the edge becomes two contact lines and two end arcs.
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[p[0], p[1], 5]], "radius": 6, "name": "Round"}));
|
||||
let (v, expect) = (volume(&mut s), (profile + gp) * 10.0);
|
||||
assert!(rel(v, expect) < 2e-4, "{v} vs {expect}");
|
||||
assert_eq!(counts(&mut s), (Some(5), Some(9), Some(6)));
|
||||
// Both corners in one fillet.
|
||||
run(
|
||||
&mut s,
|
||||
"timeline.redefine",
|
||||
json!({"feature": "Round", "command": "solid.fillet", "params": {"edges": [[p[0], p[1], 5], [q[0], q[1], 5]], "radius": 6}}),
|
||||
);
|
||||
let (v, expect) = (volume(&mut s), (profile + gp + gq) * 10.0);
|
||||
assert!(rel(v, expect) < 2e-4, "{v} vs {expect}");
|
||||
assert_eq!(counts(&mut s), (Some(6), Some(12), Some(8)));
|
||||
// A larger round, whose contacts are found on the long arcs from their far ends too.
|
||||
run(&mut s, "timeline.redefine", json!({"feature": "Round", "command": "solid.fillet", "params": {"edges": [[q[0], q[1], 5]], "radius": 13}}));
|
||||
let (v, expect) = (volume(&mut s), (profile + round_gap(c1, r1, c2, r2, q, 13.0)) * 10.0);
|
||||
assert!(rel(v, expect) < 2e-4, "{v} vs {expect}");
|
||||
}
|
||||
|
||||
/// Sketch a 40 x 30 rectangle with dimensions, extrude 20, fillet a vertical edge, cut a hole.
|
||||
#[test]
|
||||
fn box_fillet_cut_part() {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! blend's radius rolls along the edge touching both sides; its centre traces the spine and its
|
||||
//! contacts the curves where the blend meets each side. The blend face is a rational B-spline
|
||||
//! through the circular cross-sections (exact arcs at the samples), the side faces are trimmed
|
||||
//! back to the contact curves, and seams that met the edge are shortened to the contacts.
|
||||
//! back to the contact curves, and seams that met the edge (lines, or arcs of an end face) are
|
||||
//! shortened to the contacts.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -176,14 +177,47 @@ fn project(f: &mt::Face, p: Vec3, hint: Option<(f64, f64)>) -> Option<(Vec3, Vec
|
||||
Some((q, Vec3::new(n.x, n.y, n.z).normalized()?, uv))
|
||||
}
|
||||
|
||||
/// Whether `uv` lies within the parameters of the face's surface (unbounded directions pass).
|
||||
fn in_range(f: &mt::Face, uv: (f64, f64)) -> bool {
|
||||
use mt::ParametricSurface;
|
||||
use std::ops::Bound;
|
||||
let inside = |range: (Bound<f64>, Bound<f64>), x: f64| {
|
||||
let slack = |a: f64, b: f64| (b - a).abs() * 1e-6 + 1e-9;
|
||||
match range {
|
||||
(Bound::Included(a) | Bound::Excluded(a), Bound::Included(b) | Bound::Excluded(b)) => x >= a - slack(a, b) && x <= b + slack(a, b),
|
||||
(Bound::Included(a) | Bound::Excluded(a), Bound::Unbounded) => x >= a - 1e-9,
|
||||
(Bound::Unbounded, Bound::Included(b) | Bound::Excluded(b)) => x <= b + 1e-9,
|
||||
(Bound::Unbounded, Bound::Unbounded) => true,
|
||||
}
|
||||
};
|
||||
let (ru, rv) = f.oriented_surface().parameter_range();
|
||||
uv.0.is_finite() && uv.1.is_finite() && inside(ru, uv.0) && inside(rv, uv.1)
|
||||
}
|
||||
|
||||
/// Solve for the ball touching both sides near `p`. `extra` adds a third condition (a plane the
|
||||
/// side-k contact must lie in); without it the centre stays in the plane through `p` square to
|
||||
/// `t`. `sigma` is +1 when the ball sits outside the material (concave edge), −1 inside.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn solve(f1: &[&mt::Face], f2: &[&mt::Face], p: Vec3, t: Vec3, r: f64, sigma: f64, extra: Option<(usize, Vec3, Vec3)>, tol: f64) -> Option<Section> {
|
||||
// The nearest face of a side (several faces on one smooth surface group).
|
||||
// The nearest face of a side (several faces on one smooth surface group). Each face's
|
||||
// search starts from its last answer: the centre moves little between iterations. An
|
||||
// answer outside the surface's parameters, or farther than that last point (the search
|
||||
// ran away or settled on the far side), is searched again cold.
|
||||
let last = std::cell::RefCell::new(HashMap::<mt::FaceID, ((f64, f64), Vec3)>::new());
|
||||
let near = |fs: &[&mt::Face], c: Vec3| -> Option<(Vec3, Vec3)> {
|
||||
fs.iter().filter_map(|f| project(f, c, None).map(|(q, n, _)| (q, n))).min_by(|a, b| a.0.dist(c).total_cmp(&b.0.dist(c)))
|
||||
fs.iter()
|
||||
.filter_map(|f| {
|
||||
let prev = last.borrow().get(&f.id()).copied();
|
||||
let warm =
|
||||
prev.and_then(|(uv, q)| project(f, c, Some(uv)).filter(|(x, _, uv)| in_range(f, *uv) && x.dist(c) <= q.dist(c) + tol * 1e-3));
|
||||
let (q, n, uv) = match warm {
|
||||
Some(x) => x,
|
||||
None => project(f, c, None)?,
|
||||
};
|
||||
last.borrow_mut().insert(f.id(), (uv, q));
|
||||
Some((q, n))
|
||||
})
|
||||
.min_by(|a, b| a.0.dist(c).total_cmp(&b.0.dist(c)))
|
||||
};
|
||||
let (q1, n1) = near(f1, p)?;
|
||||
let (q2, n2) = near(f2, p)?;
|
||||
@@ -238,6 +272,50 @@ fn section_controls(s: &Section, round: bool) -> Option<[mt::Vector4; 3]> {
|
||||
Some([h(s.q1, 1.0), h(m, half), h(s.q2, 1.0)])
|
||||
}
|
||||
|
||||
/// A curved seam (an arc of an end face) shortened to the blend's contacts: the part of its
|
||||
/// curve between the new ends `a` and `b` (`None` keeps that end), which must lie on it, in
|
||||
/// order, short of each other.
|
||||
fn trim_curved_seam(seam: &mt::Edge, a: Option<mt::Vertex>, b: Option<mt::Vertex>, size: f64) -> Result<mt::Edge> {
|
||||
// A closed seam (a full circle starting and ending at one vertex) has no "between": which
|
||||
// end a contact replaces, and which way round the kept piece runs, are both ambiguous.
|
||||
if seam.front() == seam.back() {
|
||||
return Err(unsupported("a closed curved edge where the blend ends"));
|
||||
}
|
||||
use mt::{BoundedCurve, Cut, SearchNearestParameter};
|
||||
let mut curve = seam.curve();
|
||||
let (lo, hi) = curve.range_tuple();
|
||||
let at = |v: &Option<mt::Vertex>, end: f64| -> Result<f64> {
|
||||
let Some(v) = v else { return Ok(end) };
|
||||
// Start from the nearest of a few samples: from the far end of a long arc
|
||||
// the search can settle on the opposite side of the circle.
|
||||
let q = from_p3(v.point());
|
||||
let hint = (0..=32)
|
||||
.map(|i| lo + (hi - lo) * i as f64 / 32.0)
|
||||
.min_by(|x, y| {
|
||||
let d = |t: f64| from_p3(mt::ParametricCurve::subs(&curve, t)).dist(q);
|
||||
d(*x).total_cmp(&d(*y))
|
||||
})
|
||||
.unwrap_or(end);
|
||||
let t = curve.search_nearest_parameter(v.point(), hint, 100).ok_or_else(|| unsupported("the blend is larger than the neighbouring faces"))?;
|
||||
if !(t.is_finite() && from_p3(mt::ParametricCurve::subs(&curve, t)).dist(from_p3(v.point())) <= size * 1e-5) {
|
||||
return Err(unsupported("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
Ok(t)
|
||||
};
|
||||
let (ta, tb) = (at(&a, lo)?, at(&b, hi)?);
|
||||
let margin = (hi - lo) * 1e-9;
|
||||
if ta < lo - margin || tb > hi + margin || tb - ta < (hi - lo) * 1e-6 {
|
||||
return Err(unsupported("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
if tb < hi {
|
||||
let _ = curve.cut(tb);
|
||||
}
|
||||
let piece = if ta > lo { curve.cut(ta) } else { curve };
|
||||
let a = a.unwrap_or_else(|| seam.front().clone());
|
||||
let b = b.unwrap_or_else(|| seam.back().clone());
|
||||
mt::Edge::try_new(&a, &b, piece).map_err(|e| KernelError::Failed(format!("curve blend: seam: {e}")))
|
||||
}
|
||||
|
||||
/// Blend the closed chain of edges (ids in any order) with radius `r`.
|
||||
pub(crate) fn curve_blend(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64, round: bool) -> Result<Solid> {
|
||||
curve_blend_with_limit(solid, ids, size, r, round, MAX_SAMPLES)
|
||||
@@ -598,7 +676,8 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
let mut seam_subst: HashMap<mt::EdgeID, mt::Edge> = HashMap::new();
|
||||
for (id, (seam, a, b)) in ends {
|
||||
if !matches!(seam.curve(), mt::Curve::Line(_)) {
|
||||
return Err(unsupported("a curved edge where the blend ends"));
|
||||
seam_subst.insert(id, trim_curved_seam(&seam, a, b, size)?);
|
||||
continue;
|
||||
}
|
||||
// The new ends must stay on the seam, in order, short of each other.
|
||||
let (p0, p1) = (from_p3(seam.front().point()), from_p3(seam.back().point()));
|
||||
|
||||
@@ -192,3 +192,110 @@ fn rolling_ball_refinement_cap_returns_an_error_and_preserves_input() {
|
||||
assert_eq!(crate::step_export(&[&body], "unchanged").unwrap(), before);
|
||||
assert_eq!(crate::step_export(&[&Body::new(solid.clone()).unwrap()], "unchanged").unwrap(), input_before);
|
||||
}
|
||||
|
||||
/// Plan-view construction of a round between two overlapping circles (a concave vertical edge
|
||||
/// where two Z cylinders meet): the ball centre `f`, the circle and fillet arc angles, and the
|
||||
/// area the round fills in, all independent of the kernel.
|
||||
struct Gap {
|
||||
/// Points along the round's arc in plan, contacts included.
|
||||
arc: Vec<Vec3>,
|
||||
/// Arc angles on circle 1, circle 2 and the fillet between the corner and the contacts.
|
||||
angles: [f64; 3],
|
||||
area: f64,
|
||||
}
|
||||
|
||||
fn gap(c1: Vec3, r1: f64, c2: Vec3, r2: f64, corner: Vec3, r: f64) -> Gap {
|
||||
// The ball centre is r from both circles, on the corner's side of the centre line.
|
||||
let (a, b) = (r1 + r, r2 + r);
|
||||
let d = c1.dist(c2);
|
||||
let u = (c2 - c1) * (1.0 / d);
|
||||
let along = (a * a - b * b + d * d) / (2.0 * d);
|
||||
let mut side = Vec3::new(-u.y, u.x, 0.0);
|
||||
if (corner - c1).dot(side) < 0.0 {
|
||||
side = side * -1.0;
|
||||
}
|
||||
let f = c1 + u * along + side * (a * a - along * along).sqrt();
|
||||
let (t1, t2) = (c1 + (f - c1) * (r1 / a), c2 + (f - c2) * (r2 / b));
|
||||
let angle = |c: Vec3, p: Vec3, q: Vec3| (p - c).normalized().unwrap().dot((q - c).normalized().unwrap()).clamp(-1.0, 1.0).acos();
|
||||
let angles = [angle(c1, t1, corner), angle(c2, corner, t2), angle(f, t2, t1)];
|
||||
// Shoelace over the three short arcs: circle 1 to the corner, circle 2 on, fillet back.
|
||||
let mut ring = Vec::new();
|
||||
for (c, p, q) in [(c1, t1, corner), (c2, corner, t2), (f, t2, t1)] {
|
||||
let (p0, q0) = (p - c, q - c);
|
||||
let (a0, a1) = (p0.y.atan2(p0.x), q0.y.atan2(q0.x));
|
||||
let mut sweep = a1 - a0;
|
||||
while sweep > std::f64::consts::PI {
|
||||
sweep -= std::f64::consts::TAU;
|
||||
}
|
||||
while sweep < -std::f64::consts::PI {
|
||||
sweep += std::f64::consts::TAU;
|
||||
}
|
||||
let rr = p0.len();
|
||||
for i in 0..20_000 {
|
||||
let t = a0 + sweep * i as f64 / 20_000.0;
|
||||
ring.push(c + Vec3::new(t.cos(), t.sin(), 0.0) * rr);
|
||||
}
|
||||
}
|
||||
let area = ring.iter().zip(ring.iter().cycle().skip(1)).map(|(p, q)| p.x * q.y - q.x * p.y).sum::<f64>().abs() * 0.5;
|
||||
let (p, q) = ((t1 - f).normalized().unwrap(), (t2 - f).normalized().unwrap());
|
||||
let arc = (0..=8).map(|i| angles[2] * i as f64 / 8.0).map(|t| f + (p * ((angles[2] - t).sin()) + q * t.sin()) * (r / angles[2].sin())).collect();
|
||||
Gap { arc, angles, area }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_ball_along_a_straight_edge_between_cylinders_trims_curved_end_seams() {
|
||||
// Two Z cylinders crossing square to each other: the vertical lines where they meet are
|
||||
// concave edges between two curved faces, and the blend ends on the planar caps, where the
|
||||
// seams are arcs.
|
||||
let (r1, r2, h, r) = (17.0, 9.5, 10.0, 6.0);
|
||||
let (c1, c2) = (Vec3::ZERO, Vec3::new(r1, r2, 0.0));
|
||||
let a = cylinder(c1, Vec3::Z, r1, h).unwrap();
|
||||
let b = cylinder(c2, Vec3::Z, r2, h).unwrap();
|
||||
let body = boolean(&a, &b, BoolOp::Union).unwrap().unwrap();
|
||||
// The corner: one of the two points where the circles cross.
|
||||
let d = c1.dist(c2);
|
||||
let u = (c2 - c1) * (1.0 / d);
|
||||
let along = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
|
||||
let corner = c1 + u * along + Vec3::new(-u.y, u.x, 0.0) * (r1 * r1 - along * along).sqrt();
|
||||
let g = gap(c1, r1, c2, r2, corner, r);
|
||||
let blend = fillet(&body, &[corner + Vec3::Z * (h / 2.0)], r).unwrap();
|
||||
assert!(blend.validity().is_empty(), "{:?}", blend.validity());
|
||||
// One blend face; the edge is replaced by two contact lines and two end arcs.
|
||||
let (before, after) = (crate::measure(&body).unwrap(), crate::measure(&blend).unwrap());
|
||||
assert_eq!((after.faces, after.edges, after.vertices), (before.faces + 1, before.edges + 3, before.vertices + 2), "{before:?} -> {after:?}");
|
||||
// The round fills the gap over the full height. The caps gain it twice; the walls lose
|
||||
// their arcs to the contacts and gain the round.
|
||||
let (m0, m1) = (body.tessellate_with(0.0005, true).unwrap().measure(), blend.tessellate_with(0.0005, true).unwrap().measure());
|
||||
let volume = h * g.area;
|
||||
assert!((m1.volume - m0.volume - volume).abs() < volume * 3e-3, "added volume {} vs {volume}", m1.volume - m0.volume);
|
||||
let [a1, a2, af] = g.angles;
|
||||
let area = 2.0 * g.area + h * (r * af - r1 * a1 - r2 * a2);
|
||||
assert!((m1.area - m0.area - area).abs() < area.abs() * 3e-3, "added area {} vs {area}", m1.area - m0.area);
|
||||
// The round is the analytic arc at every height, from contact to contact.
|
||||
let surfaces: Vec<_> = blend.solid.face_iter().map(|f| f.oriented_surface()).filter(|s| matches!(s, mt::Surface::NurbsSurface(_))).collect();
|
||||
let mut worst: f64 = 0.0;
|
||||
for z in [0.0, 0.25, 0.5, 0.75, 1.0] {
|
||||
for p in &g.arc {
|
||||
worst = worst.max(nearest_blend(&surfaces, *p + Vec3::Z * (h * z)).0);
|
||||
}
|
||||
}
|
||||
assert!(worst < 1e-3, "blend surface is {worst} mm off the analytic round");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_curved_seam_is_not_supported() {
|
||||
// A closed seam (one edge starting and ending at the same vertex, as a full cap circle can
|
||||
// be in imported geometry) has no "between" two contacts, so trimming it is refused rather
|
||||
// than guessed.
|
||||
let body = cylinder(Vec3::ZERO, Vec3::Z, 10.0, 5.0).unwrap();
|
||||
let arc = body.solid.edge_iter().find(|e| !matches!(e.curve(), mt::Curve::Line(_))).expect("a cap arc");
|
||||
let circle = mt::Edge::new_unchecked(arc.front(), arc.front(), arc.curve());
|
||||
assert_eq!(circle.front(), circle.back());
|
||||
// Contacts on the circle itself: a third and two thirds of the way round.
|
||||
let (lo, hi) = circle.curve().range_tuple();
|
||||
let on = |f: f64| mt::Vertex::new(circle.curve().subs(lo + (hi - lo) * f));
|
||||
for (a, b) in [(Some(on(1.0 / 3.0)), None), (None, Some(on(2.0 / 3.0))), (Some(on(1.0 / 3.0)), Some(on(2.0 / 3.0))), (None, None)] {
|
||||
let err = trim_curved_seam(&circle, a, b, 20.0).unwrap_err();
|
||||
assert!(err.to_string().contains("not supported yet: a closed curved edge"), "{err}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user