Fillet and chamfer of one edge ending on curved, slanted or several faces (#646)
A single edge's blend no longer needs a plane square to the edge at each end. Straight edges: each rail ends where it reaches the end face's surface, the side edges there are shortened or continued along their line or circle, and the blend ends where its cylinder or chamfer plane meets that surface (a fitted curve checked against both). A chamfer may end across two faces. Curved edges: at a slanted or curved end the rolling ball runs on past the end and the strip is cut back where it meets that face. A straight edge running smoothly into a bend or rounded corner is blended along its whole smooth chain. Rolling-ball fixes: smooth chains follow edges stored against their curve, the ball touches only the faces of the edges it rolls along, the seam normal at a joint comes from the face there, and each strip is fitted with neighbouring sections at its own spacing. Action matrix: edge.fillet 83.6% -> 93.0%, edge.chamfer 82.6% -> 93.3% (697 new passes, no regressions); reference tests 76/76. Fixes #556 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9f6fb735dd
commit
8be0f64f5a
4 files changed
+1086
-463
No files matched your search
@@ -980,7 +980,6 @@ fn rectangle(plane: Value, p0: [f64; 2], p1: [f64; 2]) -> [Value; 3] {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "fails until #556 is fixed"]
|
||||
fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
|
||||
for (cmd, size) in [("solid.fillet", "radius"), ("solid.chamfer", "distance")] {
|
||||
reproduces(
|
||||
|
||||
+392
-36
@@ -198,6 +198,124 @@ pub(crate) fn extension_fits(face: &mt::Face, p: Vec3, q: Vec3, tol: f64) -> boo
|
||||
inside
|
||||
}
|
||||
|
||||
/// Parameters along `x + λ d` where the line meets `surf`, within `reach` of `x` (Newton on
|
||||
/// (u, v, λ) from several starts; nearest first).
|
||||
fn line_surface_hits(surf: &mt::Surface, x: Vec3, d: Vec3, reach: f64, tol: f64) -> Vec<f64> {
|
||||
use mt::{ParametricSurface, SearchNearestParameter};
|
||||
let mut out: Vec<f64> = Vec::new();
|
||||
for k in [0i32, 1, -1, 2, -2, 4, -4] {
|
||||
let mut l = reach * f64::from(k) / 4.0;
|
||||
let Some((mut u, mut v)) = surf.search_nearest_parameter(p3(x + d * l), None, 100) else { continue };
|
||||
for _ in 0..60 {
|
||||
let f = from_p3(surf.subs(u, v)) - (x + d * l);
|
||||
if !f.is_finite() {
|
||||
break;
|
||||
}
|
||||
if f.len() <= tol {
|
||||
if l.abs() <= reach && !out.iter().any(|o| (o - l).abs() <= tol * 10.0) {
|
||||
out.push(l);
|
||||
}
|
||||
break;
|
||||
}
|
||||
let (su, sv) = (surf.uder(u, v), surf.vder(u, v));
|
||||
let m = [[su.x, sv.x, -d.x], [su.y, sv.y, -d.y], [su.z, sv.z, -d.z]];
|
||||
let Some([du, dv, dl]) = crate::polyhedron::solve3_pub(m, [-f.x, -f.y, -f.z]) else { break };
|
||||
if ![du, dv, dl].iter().all(|z| z.is_finite()) {
|
||||
break;
|
||||
}
|
||||
// Damp long steps: the start may be far from the root.
|
||||
let step = (Vec3::new(su.x, su.y, su.z) * du + Vec3::new(sv.x, sv.y, sv.z) * dv).len().max(dl.abs());
|
||||
let k = if step > reach { reach / step } else { 1.0 };
|
||||
u += du * k;
|
||||
v += dv * k;
|
||||
l += dl * k;
|
||||
}
|
||||
}
|
||||
out.sort_by(|a, b| a.abs().total_cmp(&b.abs()));
|
||||
out
|
||||
}
|
||||
|
||||
/// A circle through the curve of `e` (centre, axis, radius), when it is one.
|
||||
fn edge_circle(e: &mt::Edge, tol: f64) -> Option<(Vec3, Vec3, f64)> {
|
||||
use mt::{BoundedCurve, ParametricCurve};
|
||||
let c = e.curve();
|
||||
let (lo, hi) = c.range_tuple();
|
||||
let at = |f: f64| from_p3(c.subs(lo + (hi - lo) * f));
|
||||
let (a, b, m) = (at(0.0), at(1.0), at(0.5));
|
||||
let (u, w) = (b - a, m - a);
|
||||
let n = u.cross(w);
|
||||
let n2 = n.len2();
|
||||
if !(n2 > 1e-30) {
|
||||
return None;
|
||||
}
|
||||
// Circumcentre of a, b, m.
|
||||
let centre = a + (n.cross(u) * w.len2() + w.cross(n) * u.len2()) / (2.0 * n2);
|
||||
let radius = centre.dist(a);
|
||||
let axis = n.normalized()?;
|
||||
let ok = (1..16).all(|i| {
|
||||
let q = at(f64::from(i) / 16.0);
|
||||
(q.dist(centre) - radius).abs() <= tol && (q - centre).dot(axis).abs() <= tol
|
||||
});
|
||||
(ok && radius.is_finite()).then_some((centre, axis, radius))
|
||||
}
|
||||
|
||||
/// Where `q` lies on the edge's curve: its parameter, if within `tol` of the curve and inside
|
||||
/// the edge's range.
|
||||
pub(crate) fn on_edge(e: &mt::Edge, q: Vec3, tol: f64) -> Option<f64> {
|
||||
use mt::{BoundedCurve, ParametricCurve, SearchNearestParameter};
|
||||
let c = e.curve();
|
||||
let (lo, hi) = c.range_tuple();
|
||||
let hint =
|
||||
(0..=32).map(|i| lo + (hi - lo) * f64::from(i) / 32.0).min_by(|x, y| from_p3(c.subs(*x)).dist(q).total_cmp(&from_p3(c.subs(*y)).dist(q)))?;
|
||||
let t = c.search_nearest_parameter(p3(q), hint, 100)?;
|
||||
let margin = (hi - lo) * 1e-9;
|
||||
(t.is_finite() && t >= lo - margin && t <= hi + margin && from_p3(c.subs(t)).dist(q) <= tol).then_some(t.clamp(lo, hi))
|
||||
}
|
||||
|
||||
/// Whether `q` lies on the edge's curve continued past its ends (a line or a circle).
|
||||
pub(crate) fn on_extension(e: &mt::Edge, q: Vec3, tol: f64) -> bool {
|
||||
match e.curve() {
|
||||
mt::Curve::Line(_) => {
|
||||
let (a, b) = (vtx(e.absolute_front()), vtx(e.absolute_back()));
|
||||
(b - a).normalized().is_some_and(|u| (q - a - u * (q - a).dot(u)).len() <= tol)
|
||||
}
|
||||
_ => edge_circle(e, tol).is_some_and(|(c, n, r)| ((q - c).dot(n)).abs() <= tol && ((q - c - n * (q - c).dot(n)).len() - r).abs() <= tol),
|
||||
}
|
||||
}
|
||||
|
||||
/// The edge `e` with its end at `v` moved to `nv`, along its own curve: shortened (any curve),
|
||||
/// or continued past `v` (a line or a circle). In the stored orientation of `e`.
|
||||
pub(crate) fn retarget(e: &mt::Edge, v: &mt::Vertex, nv: &mt::Vertex, tol: f64) -> Result<mt::Edge> {
|
||||
let (f, bk) = (e.absolute_front(), e.absolute_back());
|
||||
if f == bk {
|
||||
return Err(unsupported("a closed edge where the blend ends"));
|
||||
}
|
||||
if matches!(e.curve(), mt::Curve::Line(_)) {
|
||||
return Ok(if f == v { builder::line(nv, bk) } else { builder::line(f, nv) });
|
||||
}
|
||||
use mt::Cut;
|
||||
let q = vtx(nv);
|
||||
if let Some(t) = on_edge(e, q, tol) {
|
||||
use mt::BoundedCurve;
|
||||
let mut c = e.curve();
|
||||
// The piece kept must not shrink to nothing.
|
||||
let (lo, hi) = c.range_tuple();
|
||||
let margin = (hi - lo) * 1e-6;
|
||||
if (f == v && t >= hi - margin) || (f != v && t <= lo + margin) {
|
||||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
let rest = c.cut(t);
|
||||
let (piece, a, b) = if f == v { (rest, nv, bk) } else { (c, f, nv) };
|
||||
return mt::Edge::try_new(a, b, piece).map_err(|err| KernelError::Failed(format!("blend: corner edge: {err}")));
|
||||
}
|
||||
// Continued past v: the arc from the new end through v to the far end.
|
||||
if edge_circle(e, tol).is_some() && on_extension(e, q, tol) {
|
||||
let through = p3(vtx(v));
|
||||
return Ok(if f == v { builder::circle_arc(nv, bk, through) } else { builder::circle_arc(f, nv, through) });
|
||||
}
|
||||
Err(too_large("the blend is larger than the neighbouring faces"))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn blend_geometry(
|
||||
solid: &Solid,
|
||||
@@ -237,6 +355,10 @@ fn blend_geometry(
|
||||
planes: Vec<(Vec3, Vec3)>,
|
||||
ca: Option<mt::Vertex>,
|
||||
cb: Option<mt::Vertex>,
|
||||
/// Points of the end's section on a curved end face, from A to B.
|
||||
cap: Option<Vec<Vec3>>,
|
||||
/// How far along the edge the section reaches from the end vertex.
|
||||
reach: f64,
|
||||
}
|
||||
let mut ends: Vec<End> = Vec::new();
|
||||
for (v, p) in [(&v0, p0), (&v1, p1)] {
|
||||
@@ -249,10 +371,98 @@ fn blend_geometry(
|
||||
};
|
||||
let (ga, gb) = match others[..] {
|
||||
[g] => (g, g),
|
||||
[x, y] if shape == Shape::Round && shared(i1, x).is_some() && shared(i2, y).is_some() => (x, y),
|
||||
[x, y] if shape == Shape::Round && shared(i1, y).is_some() && shared(i2, x).is_some() => (y, x),
|
||||
[x, y] if shared(i1, x).is_some() && shared(i2, y).is_some() => (x, y),
|
||||
[x, y] if shared(i1, y).is_some() && shared(i2, x).is_some() => (y, x),
|
||||
_ => return Err(unsupported("each end of the edge must touch exactly one more face")),
|
||||
};
|
||||
// An end face that is not a plane, or meets the side faces in curves: the rails end where
|
||||
// they reach its surface, and the blend's end is where its surface meets the end face
|
||||
// (the cross-section through `p` slid along d onto it, point by point).
|
||||
let is_line = |e: &mt::Edge| matches!(e.curve(), mt::Curve::Line(_));
|
||||
if ga == gb
|
||||
&& let (Some(e1), Some(e2)) = (shared(i1, ga), shared(i2, gb))
|
||||
&& (faces.get(ga).and_then(|f| plane_normal(f)).is_none() || !is_line(&e1) || !is_line(&e2))
|
||||
{
|
||||
let g = faces.get(ga).ok_or_else(|| KernelError::Failed("face".into()))?;
|
||||
let surf = g.surface();
|
||||
let on_tol = (size * 1e-5).max(1e-6);
|
||||
let reach = 3.0 * (s + r);
|
||||
let (sa, sb) = (p + t1 * s, p + t2 * s);
|
||||
let c0 = centre(p);
|
||||
let theta = std::f64::consts::PI - phi;
|
||||
let profile = |f: f64| -> Vec3 {
|
||||
match shape {
|
||||
Shape::Flat => sa + (sb - sa) * f,
|
||||
Shape::Round => c0 + ((sa - c0) * ((1.0 - f) * theta).sin() + (sb - c0) * (f * theta).sin()) / theta.sin(),
|
||||
}
|
||||
};
|
||||
// A rail's end: on the side edge, else on its continuation within the side face.
|
||||
let rail_end = |x: Vec3, e: &mt::Edge, fi: usize| -> Result<(Vec3, f64)> {
|
||||
let hits = line_surface_hits(&surf, x, d, reach, tol);
|
||||
if let Some(l) = hits.iter().find(|l| on_edge(e, x + d * **l, on_tol).is_some()) {
|
||||
return Ok((x + d * *l, *l));
|
||||
}
|
||||
let side = faces.get(fi).ok_or_else(|| KernelError::Failed("face".into()))?;
|
||||
hits.iter()
|
||||
.find(|l| on_extension(e, x + d * **l, on_tol) && extension_fits(side, p, x + d * **l, tol))
|
||||
.map(|l| (x + d * *l, *l))
|
||||
.ok_or_else(|| too_large("the blend is larger than the neighbouring faces"))
|
||||
};
|
||||
let ((a, la), (b, lb)) = (rail_end(sa, &e1, i1)?, rail_end(sb, &e2, i2)?);
|
||||
let flat_plane = shape == Shape::Flat && plane_normal(g).is_some();
|
||||
let mut cap: Option<Vec<Vec3>> = None;
|
||||
let mut far = la.abs().max(lb.abs());
|
||||
for n in [16usize, 32, 64] {
|
||||
if flat_plane {
|
||||
cap = Some(vec![a, b]);
|
||||
break;
|
||||
}
|
||||
// The section's points on the end face, each continuing from the last.
|
||||
let mut pts = vec![a];
|
||||
let mut last = la;
|
||||
let mut ok = true;
|
||||
for i in 1..n {
|
||||
let x = profile(i as f64 / n as f64);
|
||||
match line_surface_hits(&surf, x, d, reach, tol).into_iter().min_by(|u, v| (u - last).abs().total_cmp(&(v - last).abs())) {
|
||||
Some(l) => {
|
||||
pts.push(x + d * l);
|
||||
last = l;
|
||||
far = far.max(l.abs());
|
||||
}
|
||||
None => {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
break;
|
||||
}
|
||||
pts.push(b);
|
||||
// The fitted curve must stay on both surfaces between the points.
|
||||
let Some(bs) = crate::build::interpolate_cubic(&pts) else { break };
|
||||
let fits = (0..n).all(|i| {
|
||||
use mt::{ParametricCurve, ParametricSurface, SearchNearestParameter};
|
||||
let q = from_p3(bs.subs((i as f64 + 0.5) / n as f64));
|
||||
let on_g = surf.search_nearest_parameter(p3(q), None, 100).is_some_and(|(u, v)| from_p3(surf.subs(u, v)).dist(q) <= on_tol);
|
||||
let on_blend = match shape {
|
||||
Shape::Round => {
|
||||
let w = q - c0;
|
||||
((w - d * w.dot(d)).len() - r).abs() <= on_tol
|
||||
}
|
||||
Shape::Flat => (sb - sa).cross(d).normalized().is_some_and(|m| (q - sa).dot(m).abs() <= on_tol),
|
||||
};
|
||||
on_g && on_blend
|
||||
});
|
||||
if fits {
|
||||
cap = Some(pts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let cap = cap.ok_or_else(|| unsupported("the blend's end on a curved face"))?;
|
||||
ends.push(End { v: v.clone(), g: vec![ga], e1, e2, e3: None, a, b, planes: Vec::new(), ca: None, cb: None, cap: Some(cap), reach: far });
|
||||
continue;
|
||||
}
|
||||
let normal = |g: usize| -> Result<Vec3> {
|
||||
let n = faces.get(g).and_then(|f| plane_normal(f)).ok_or_else(|| unsupported("the faces at the edge ends must be planar"))?;
|
||||
if n.dot(d).abs() < 1e-9 {
|
||||
@@ -311,6 +521,33 @@ fn blend_geometry(
|
||||
let (len3, u) = (far.dist(p), (far - p).normalized().ok_or_else(|| unsupported("zero-length corner edge"))?);
|
||||
let c0 = centre(p);
|
||||
let perp = |x: Vec3| x - d * x.dot(d);
|
||||
if shape == Shape::Flat {
|
||||
// The chamfer plane through both rails crosses the edge between them.
|
||||
let (sa, sb) = (p + t1 * s, p + t2 * s);
|
||||
let m = (sb - sa).cross(d).normalized().ok_or_else(|| KernelError::Failed("chamfer plane".into()))?;
|
||||
let den = m.dot(u);
|
||||
let root = if den.abs() > 1e-12 { m.dot(sa - p) / den } else { f64::NAN };
|
||||
let x = perp(p + u * root - sa);
|
||||
let along = x.dot(sb - sa) / (sb - sa).len2();
|
||||
if !(root > tol && root < len3 - tol && along > 1e-9 && along < 1.0 - 1e-9) {
|
||||
return Err(unsupported("the blend does not cross the edge between the end faces"));
|
||||
}
|
||||
ends.push(End {
|
||||
v: v.clone(),
|
||||
g: vec![ga, gb],
|
||||
e1: e1.clone(),
|
||||
e2: e2.clone(),
|
||||
e3: Some((e3.clone(), p + u * root)),
|
||||
a,
|
||||
b,
|
||||
planes: vec![(p, na), (p, nb)],
|
||||
ca: None,
|
||||
cb: None,
|
||||
cap: None,
|
||||
reach: 0.0,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let (w, up) = (perp(p - c0), perp(u));
|
||||
// |w + λ up|² = r²
|
||||
let (qa, qb, qc) = (up.len2(), 2.0 * w.dot(up), w.len2() - r * r);
|
||||
@@ -330,7 +567,7 @@ fn blend_geometry(
|
||||
};
|
||||
let planes = if ga == gb { vec![(p, na)] } else { vec![(p, na), (p, nb)] };
|
||||
let [ca, cb] = consumed;
|
||||
ends.push(End { v: v.clone(), g: if ga == gb { vec![ga] } else { vec![ga, gb] }, e1, e2, e3, a, b, planes, ca, cb });
|
||||
ends.push(End { v: v.clone(), g: if ga == gb { vec![ga] } else { vec![ga, gb] }, e1, e2, e3, a, b, planes, ca, cb, cap: None, reach: 0.0 });
|
||||
}
|
||||
let [end0, end1] = &ends[..] else { return Err(KernelError::Failed("ends".into())) };
|
||||
if end0.g.iter().any(|g| end1.g.contains(g)) {
|
||||
@@ -393,6 +630,16 @@ fn blend_geometry(
|
||||
let (va, vb) = (vertex(&end.ca, end.a), vertex(&end.cb, end.b));
|
||||
let (sa, sb) = (p + t1 * s, p + t2 * s);
|
||||
let (chain, vq) = match (&end.e3, &end.planes[..]) {
|
||||
_ if end.cap.is_some() => {
|
||||
let pts = end.cap.as_deref().unwrap_or_default();
|
||||
let curve = if pts.len() == 2 {
|
||||
builder::line(&va, &vb)
|
||||
} else {
|
||||
let bs = crate::build::interpolate_cubic(pts).ok_or_else(|| unsupported("the blend's end on a curved face"))?;
|
||||
mt::Edge::try_new(&va, &vb, mt::Curve::BSplineCurve(bs)).map_err(|e| KernelError::Failed(format!("blend end: {e}")))?
|
||||
};
|
||||
(vec![curve], None)
|
||||
}
|
||||
(None, [plane]) => (vec![section(p, &va, &vb, sa, sb, *plane)?], None),
|
||||
(Some((_, q)), [pa, pb]) => {
|
||||
let vq = builder::vertex(p3(*q));
|
||||
@@ -411,20 +658,18 @@ fn blend_geometry(
|
||||
// between two end faces stops where the blend crosses it; the blended edge moves to A or B.
|
||||
let mut subst: HashMap<mt::EdgeID, mt::Edge> = HashMap::new();
|
||||
let mut dropped: Vec<mt::EdgeID> = Vec::new();
|
||||
let shorten = |e: &mt::Edge, v: &mt::Vertex, nv: &mt::Vertex| {
|
||||
let (f, bk) = (e.absolute_front(), e.absolute_back());
|
||||
if f == v { builder::line(nv, bk) } else { builder::line(f, nv) }
|
||||
};
|
||||
let on_tol = (size * 1e-5).max(1e-6);
|
||||
let shorten = |e: &mt::Edge, v: &mt::Vertex, nv: &mt::Vertex| retarget(e, v, nv, on_tol);
|
||||
for (end, a, b, q) in [(end0, a0, b0, q0), (end1, a1, b1, q1)] {
|
||||
for (e, nv, used) in [(&end.e1, a, &end.ca), (&end.e2, b, &end.cb)] {
|
||||
if used.is_some() {
|
||||
dropped.push(e.id());
|
||||
continue;
|
||||
}
|
||||
subst.insert(e.id(), shorten(e, &end.v, nv));
|
||||
subst.insert(e.id(), shorten(e, &end.v, nv)?);
|
||||
}
|
||||
if let (Some((e3, _)), Some(q)) = (&end.e3, q) {
|
||||
subst.insert(e3.id(), shorten(e3, &end.v, q));
|
||||
subst.insert(e3.id(), shorten(e3, &end.v, q)?);
|
||||
}
|
||||
}
|
||||
// The side edges at the ends are pieces of themselves; the rails and corner curves are new.
|
||||
@@ -479,6 +724,7 @@ fn blend_geometry(
|
||||
let reach = ends
|
||||
.iter()
|
||||
.flat_map(|e| e.planes.iter().map(|(_, n)| (s + r) * (1.0 - n.dot(d).powi(2)).max(0.0).sqrt() / n.dot(d).abs()))
|
||||
.chain(ends.iter().map(|e| e.reach))
|
||||
.fold(0.0, f64::max)
|
||||
+ r;
|
||||
let start = p0 - d * reach;
|
||||
@@ -601,32 +847,45 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul
|
||||
return Err(KernelError::Invalid(format!("no edge near {:?}", [p.x, p.y, p.z])));
|
||||
}
|
||||
let edge = Body::unique_edges(&solid).into_iter().nth(idx).ok_or_else(|| KernelError::Invalid("edge index".into()))?;
|
||||
let attempt = match guard(what, || blend_one(&solid, &edge, size, r, shape)) {
|
||||
Ok(o) => Ok((o, vec![j])),
|
||||
// A curved edge, or one between curved faces: roll a ball along its smooth chain.
|
||||
Err(e) if !curved_case(&solid, &edge) => Err(e),
|
||||
Err(e) => match smooth_chain(&solid, &edge) {
|
||||
Some(ids) => {
|
||||
let on_chain: Vec<usize> = todo
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, q)| {
|
||||
cur.nearest_edge(**q, tol)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|(i, _)| Body::unique_edges(&solid).into_iter().nth(i))
|
||||
.is_some_and(|x| ids.contains(&x.id()))
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
guard(what, || crate::curveblend::curve_blend(&solid, &ids, size, r, shape == Shape::Round))
|
||||
.map(|o| (o, if on_chain.is_empty() { vec![j] } else { on_chain }))
|
||||
.map_err(|e2| match e2 {
|
||||
KernelError::TooLarge(m) => KernelError::TooLarge(format!("{m} ({e})")),
|
||||
e2 => KernelError::Failed(format!("{e2} ({e})")),
|
||||
})
|
||||
}
|
||||
None => Err(e),
|
||||
// Roll a ball along the edge's smooth chain (the points of `todo` on it go with it).
|
||||
let roll = |ids: &[mt::EdgeID]| {
|
||||
let on_chain: Vec<usize> = todo
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, q)| {
|
||||
cur.nearest_edge(**q, tol)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|(i, _)| Body::unique_edges(&solid).into_iter().nth(i))
|
||||
.is_some_and(|x| ids.contains(&x.id()))
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
guard(what, || crate::curveblend::curve_blend(&solid, ids, size, r, shape == Shape::Round))
|
||||
.map(|o| (o, if on_chain.is_empty() { vec![j] } else { on_chain }))
|
||||
};
|
||||
// A straight edge running on smoothly into curved ones (a flange into its bend, a
|
||||
// side into a rounded corner): the blend follows the whole smooth chain, as the
|
||||
// rolling ball would.
|
||||
let chain = smooth_chain(&solid, &edge);
|
||||
let runs_on = !curved_case(&solid, &edge)
|
||||
&& chain.as_ref().is_some_and(|ids| {
|
||||
ids.len() > 1 && Body::unique_edges(&solid).iter().any(|x| ids.contains(&x.id()) && !matches!(x.curve(), mt::Curve::Line(_)))
|
||||
});
|
||||
let attempt = match chain.as_deref().filter(|_| runs_on).map(roll) {
|
||||
Some(Ok(o)) => Ok(o),
|
||||
Some(Err(e2)) => guard(what, || blend_one(&solid, &edge, size, r, shape)).map(|o| (o, vec![j])).map_err(|_| e2),
|
||||
None => match guard(what, || blend_one(&solid, &edge, size, r, shape)) {
|
||||
Ok(o) => Ok((o, vec![j])),
|
||||
// A curved edge, or one between curved faces: roll a ball along its smooth chain.
|
||||
Err(e) if !curved_case(&solid, &edge) => Err(e),
|
||||
Err(e) => match chain.as_deref() {
|
||||
Some(ids) => roll(ids).map_err(|e2| match e2 {
|
||||
KernelError::TooLarge(m) => KernelError::TooLarge(format!("{m} ({e})")),
|
||||
e2 => KernelError::Failed(format!("{e2} ({e})")),
|
||||
}),
|
||||
None => Err(e),
|
||||
},
|
||||
},
|
||||
};
|
||||
match attempt.and_then(|((o, rec), js)| record::rebuilt(o, &[&solid], &rec).map(|b| (b, js))) {
|
||||
@@ -761,8 +1020,9 @@ fn curved_case(solid: &Solid, edge: &mt::Edge) -> bool {
|
||||
fn smooth_chain(solid: &Solid, edge: &mt::Edge) -> Option<Vec<mt::EdgeID>> {
|
||||
use mt::{BoundedCurve, ParametricCurve};
|
||||
let all = Body::unique_edges(solid);
|
||||
// Along the edge as it runs from its front to its back (an edge may run against its curve).
|
||||
let dir = |e: &mt::Edge, at_end: bool| -> Option<Vec3> {
|
||||
let c = e.curve();
|
||||
let c = e.oriented_curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
let d = c.der(if at_end { t1 } else { t0 });
|
||||
Vec3::new(d.x, d.y, d.z).normalized()
|
||||
@@ -1352,6 +1612,102 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A box with a cylinder joined across one side (#556): the bottom edge of that side runs
|
||||
/// into the cylinder, which is curved where the edge ends.
|
||||
fn box_and_boss() -> Body {
|
||||
let b = crate::build::box_solid(Vec3::ZERO, Vec3::new(40.0, 40.0, 20.0)).unwrap();
|
||||
let c = crate::build::cylinder(Vec3::new(40.0, 20.0, 0.0), Vec3::Z, 12.0, 40.0).unwrap();
|
||||
crate::ops::boolean(&b, &c, crate::BoolOp::Union).unwrap().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_blend_runs_into_a_cylinder_at_its_end() {
|
||||
let body = box_and_boss();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
// The edge (40, 0..8, 0) ends in the cylinder wall at y = 8.
|
||||
let len = 20.0 - (144.0f64 - 0.0).sqrt();
|
||||
for (shape, size, area) in [(Shape::Round, 0.5, (1.0 - std::f64::consts::PI / 4.0) * 0.25), (Shape::Flat, 0.5, 0.125)] {
|
||||
let out = blend(&body, &[Vec3::new(40.0, 4.0, 0.0)], size, shape, "blend").unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
let removed = v0 - vol(&out);
|
||||
// The section runs on a little past y = 8 under the cylinder wall.
|
||||
assert!(removed > area * len * 0.99 && removed < area * (len + 0.02), "{removed} vs {}", area * len);
|
||||
assert_eq!(out.face_count(), body.face_count() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_blend_ends_on_a_hole_wall() {
|
||||
// A plate with a hole cutting through the middle of its top front edge: each piece of
|
||||
// that edge ends on the hole's wall.
|
||||
let plate = crate::build::box_solid(Vec3::ZERO, Vec3::new(40.0, 20.0, 10.0)).unwrap();
|
||||
let hole = crate::build::cylinder(Vec3::new(20.0, 0.0, -1.0), Vec3::Z, 5.0, 12.0).unwrap();
|
||||
let body = crate::ops::boolean(&plate, &hole, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
for (shape, area) in [(Shape::Round, (1.0 - std::f64::consts::PI / 4.0)), (Shape::Flat, 0.5)] {
|
||||
let out = blend(&body, &[Vec3::new(5.0, 0.0, 10.0)], 1.0, shape, "blend").unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
let removed = v0 - vol(&out);
|
||||
assert!(removed > area * 14.0 && removed < area * 15.2, "{removed}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_chamfer_ends_across_two_faces() {
|
||||
// A plate's outline chamfered, then the mitre at a corner chamfered: its low end meets
|
||||
// both side faces, and the chamfer plane cuts back the vertical edge between them.
|
||||
let k = 5.0;
|
||||
let b = crate::build::box_solid(Vec3::ZERO, Vec3::new(100.0, 50.0, 20.0)).unwrap();
|
||||
let outline = [Vec3::new(50.0, 0.0, 20.0), Vec3::new(100.0, 25.0, 20.0), Vec3::new(50.0, 50.0, 20.0), Vec3::new(0.0, 25.0, 20.0)];
|
||||
let c = chamfer(&b, &outline, k).unwrap();
|
||||
let out = blend(&c, &[Vec3::new(k / 2.0, k / 2.0, 20.0 - k / 2.0)], 1.0, Shape::Flat, "chamfer").unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
assert_eq!(out.lumps().unwrap().len(), 1);
|
||||
assert_eq!(out.face_count(), c.face_count() + 1);
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
// About the triangle (sides 1 and 1 at 120°) along the mitre; its ends slant.
|
||||
let removed = vol(&c) - vol(&out);
|
||||
let along = 0.5 * 120f64.to_radians().sin() * k * 3f64.sqrt();
|
||||
assert!(removed > along * 0.8 && removed < along * 1.2, "{removed} vs {along}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_curved_edge_blend_ends_in_a_face_at_an_angle() {
|
||||
// A groove of radius 8 along y with its axis 4 below the top: the arcs where it meets the
|
||||
// front and back faces end in the top face at 60° to it.
|
||||
let b = crate::build::box_solid(Vec3::ZERO, Vec3::new(40.0, 20.0, 20.0)).unwrap();
|
||||
let c = crate::build::cylinder(Vec3::new(20.0, -1.0, 16.0), Vec3::Y, 8.0, 22.0).unwrap();
|
||||
let body = crate::ops::boolean(&b, &c, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
for shape in [Shape::Round, Shape::Flat] {
|
||||
let out = blend(&body, &[Vec3::new(20.0, 0.0, 8.0)], 0.5, shape, "blend").unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
// One blend face per piece of the arc (the groove's surface is in two faces).
|
||||
assert_eq!(out.face_count(), body.face_count() + 2);
|
||||
// About the section along the arc (240°, at the section's distance from the axis).
|
||||
let area = if shape == Shape::Round { (1.0 - std::f64::consts::PI / 4.0) * 0.25 } else { 0.125 };
|
||||
let along = 8.2 * 240f64.to_radians();
|
||||
let removed = v0 - vol(&out);
|
||||
assert!(removed > area * along * 0.9 && removed < area * along * 1.1, "{removed} vs {}", area * along);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_blend_of_an_edge_running_into_a_rounded_corner_follows_it() {
|
||||
// A box with one vertical edge rounded: a top edge meeting that round smoothly is blended
|
||||
// with the round's top arc and the next top edge, as one smooth chain.
|
||||
let b = crate::build::box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap();
|
||||
let rounded = blend(&b, &[Vec3::new(40.0, 30.0, 10.0)], 5.0, Shape::Round, "fillet").unwrap();
|
||||
for shape in [Shape::Round, Shape::Flat] {
|
||||
let out = blend(&rounded, &[Vec3::new(20.0, 30.0, 20.0)], 1.0, shape, "blend").unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
assert_eq!(out.face_count(), rounded.face_count() + 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_chamfer_intersects_sloped_end_planes() {
|
||||
for slope in [-0.5, 0.5] {
|
||||
|
||||
+293
-25
@@ -24,6 +24,8 @@ fn unsupported(msg: &str) -> KernelError {
|
||||
const SAMPLES: usize = 24;
|
||||
const MAX_SAMPLES: usize = 369;
|
||||
const MAX_CHAIN_SAMPLES: usize = 100_000;
|
||||
/// Sections past a slanted end of an open chain.
|
||||
const EXTENSION: usize = 8;
|
||||
|
||||
struct Fit {
|
||||
surface: mt::Surface,
|
||||
@@ -72,21 +74,32 @@ impl Guide {
|
||||
/// One interpolation for both the face and its boundary curves. Fitting the contacts
|
||||
/// independently gives different end conditions and leaves gaps between the same faces.
|
||||
fn fit_sections(sections: &[Vec<Section>], k: usize, closed: bool, round: bool) -> Result<Fit> {
|
||||
use mt::Cut;
|
||||
let fail = |m: &str| KernelError::Failed(format!("curve blend: {m}"));
|
||||
let n = sections.len();
|
||||
let list = sections.get(k).ok_or_else(|| fail("section"))?;
|
||||
let before = usize::from(closed || k > 0) * 2;
|
||||
let mut ext = Vec::with_capacity(list.len() + 4);
|
||||
if before > 0 {
|
||||
let mut before = Vec::new();
|
||||
if closed || k > 0 {
|
||||
let prev = sections.get((k + n - 1) % n).ok_or_else(|| fail("section"))?;
|
||||
ext.extend(prev.iter().rev().skip(1).take(2).rev().copied());
|
||||
before.extend(prev.iter().rev().skip(1).take(2).rev().copied());
|
||||
}
|
||||
ext.extend(list.iter().copied());
|
||||
let mut after = Vec::new();
|
||||
if closed || k + 1 < n {
|
||||
let next = sections.get((k + 1) % n).ok_or_else(|| fail("section"))?;
|
||||
ext.extend(next.iter().skip(1).take(2).copied());
|
||||
after.extend(next.iter().skip(1).take(2).copied());
|
||||
}
|
||||
fit_strip(list, &before, &after, round)
|
||||
}
|
||||
|
||||
/// [`fit_sections`] with the neighbouring sections given: best spaced as the strip's own, since
|
||||
/// the interpolation's parameters are uniform.
|
||||
fn fit_strip(list: &[Section], before: &[Section], after: &[Section], round: bool) -> Result<Fit> {
|
||||
use mt::Cut;
|
||||
let fail = |m: &str| KernelError::Failed(format!("curve blend: {m}"));
|
||||
let mut ext = Vec::with_capacity(list.len() + before.len() + after.len());
|
||||
ext.extend(before.iter().copied());
|
||||
ext.extend(list.iter().copied());
|
||||
ext.extend(after.iter().copied());
|
||||
let before = before.len();
|
||||
let ctrl: Vec<[mt::Vector4; 3]> = ext.iter().map(|s| section_controls(s, round)).collect::<Option<_>>().ok_or_else(|| fail("section"))?;
|
||||
let mut rows = Vec::with_capacity(3);
|
||||
let mut vknots = None;
|
||||
@@ -161,6 +174,88 @@ fn section_fits(fit: &Fit, section: &Section, along: f64, round: bool, sigma: f6
|
||||
true
|
||||
}
|
||||
|
||||
/// Where a blend strip run on past a chain end meets the end face: the contacts' parameters
|
||||
/// there and the end curve from contact 1 to contact 2 (fitted through points where the
|
||||
/// strip's cross-section curves meet the face's surface, and checked against both).
|
||||
fn end_cap(fit: &Fit, g: &mt::Face, at: &Section, round: bool, tolerance: f64) -> Result<(f64, f64, mt::Curve)> {
|
||||
use mt::{ParametricCurve, ParametricSurface, SearchNearestParameter};
|
||||
let fail = || unsupported("the blend's end on its end face");
|
||||
let (surf, gs) = (&fit.surface, g.surface());
|
||||
let mid = if round { at.c + ((at.q1 - at.c) + (at.q2 - at.c)).normalized().ok_or_else(fail)? * at.c.dist(at.q1) } else { (at.q1 + at.q2) * 0.5 };
|
||||
let (_, v_start) = surf.search_nearest_parameter(p3(mid), None, 100).ok_or_else(fail)?;
|
||||
// S(u, v) = G(a, b) for v, a, b by Newton from v.
|
||||
let meet = |u: f64, v: f64| -> Option<f64> {
|
||||
let mut v = v;
|
||||
let (mut a, mut b) = gs.search_nearest_parameter(surf.subs(u, v), None, 100)?;
|
||||
for _ in 0..60 {
|
||||
let f = from_p3(surf.subs(u, v)) - from_p3(gs.subs(a, b));
|
||||
if !f.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if f.len() <= tolerance * 1e-3 {
|
||||
return (-1e-9..=1.0 + 1e-9).contains(&v).then_some(v.clamp(0.0, 1.0));
|
||||
}
|
||||
let (sv, ga, gb) = (surf.vder(u, v), gs.uder(a, b), gs.vder(a, b));
|
||||
let m = [[sv.x, -ga.x, -gb.x], [sv.y, -ga.y, -gb.y], [sv.z, -ga.z, -gb.z]];
|
||||
let [dv, da, db] = crate::polyhedron::solve3_pub(m, [-f.x, -f.y, -f.z])?;
|
||||
// Keep each step within the strip's parameters.
|
||||
let k = if dv.abs() > 0.1 { 0.1 / dv.abs() } else { 1.0 };
|
||||
v += dv * k;
|
||||
a += da * k;
|
||||
b += db * k;
|
||||
}
|
||||
None
|
||||
};
|
||||
for m in [16usize, 32, 64] {
|
||||
let mut pts = Vec::with_capacity(m + 1);
|
||||
let mut vs = Vec::with_capacity(m + 1);
|
||||
// From the middle out to both contacts, each search starting where the last ended.
|
||||
let half = m / 2;
|
||||
let mid_v = meet(0.5_f64.min(half as f64 / m as f64), v_start).ok_or_else(fail)?;
|
||||
let mut found = vec![None; m + 1];
|
||||
if let Some(slot) = found.get_mut(half) {
|
||||
*slot = Some(mid_v);
|
||||
}
|
||||
let mut last = mid_v;
|
||||
for i in (0..half).rev() {
|
||||
last = meet(i as f64 / m as f64, last).ok_or_else(fail)?;
|
||||
if let Some(slot) = found.get_mut(i) {
|
||||
*slot = Some(last);
|
||||
}
|
||||
}
|
||||
last = mid_v;
|
||||
for i in half + 1..=m {
|
||||
last = meet(i as f64 / m as f64, last).ok_or_else(fail)?;
|
||||
if let Some(slot) = found.get_mut(i) {
|
||||
*slot = Some(last);
|
||||
}
|
||||
}
|
||||
for (i, v) in found.iter().enumerate() {
|
||||
let v = v.ok_or_else(fail)?;
|
||||
vs.push(v);
|
||||
pts.push(from_p3(surf.subs(i as f64 / m as f64, v)));
|
||||
}
|
||||
let bs = crate::build::interpolate_cubic(&pts).ok_or_else(fail)?;
|
||||
let fits = (0..m).all(|i| {
|
||||
let q = p3(from_p3(bs.subs((i as f64 + 0.5) / m as f64)));
|
||||
let hint = vs.get(i).map(|v| ((i as f64 + 0.5) / m as f64, *v));
|
||||
let on_g = gs.search_nearest_parameter(q, None, 100).is_some_and(|(a, b)| from_p3(gs.subs(a, b)).dist(from_p3(q)) <= tolerance);
|
||||
let on_s = surf.search_nearest_parameter(q, hint, 100).is_some_and(|(u, v)| from_p3(surf.subs(u, v)).dist(from_p3(q)) <= tolerance);
|
||||
on_g && on_s
|
||||
});
|
||||
if fits {
|
||||
let (Some(t1), Some(t2)) = (vs.first(), vs.last()) else { return Err(fail()) };
|
||||
// Both contacts are cut there: strictly inside the strip's own part.
|
||||
let inside = |t: f64| t > fit.range.0 + 1e-6 && t < fit.range.1 - 1e-6;
|
||||
if !(inside(*t1) && inside(*t2)) {
|
||||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
return Ok((*t1, *t2, mt::Curve::BSplineCurve(bs)));
|
||||
}
|
||||
}
|
||||
Err(fail())
|
||||
}
|
||||
|
||||
/// One cross-section of the blend: ball centre and its contacts with side 1 and side 2.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Section {
|
||||
@@ -409,6 +504,35 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
}
|
||||
let s1: Vec<&mt::Face> = side1.iter().filter_map(|i| faces.get(*i)).collect();
|
||||
let s2: Vec<&mt::Face> = side2.iter().filter_map(|i| faces.get(*i)).collect();
|
||||
// The faces the ball may touch near edges `ks` of the chain: their own, on each side. (The
|
||||
// surface of another face of a side, such as a wall's plane past the bend it runs into,
|
||||
// may pass nearer than the face the ball actually rolls on.)
|
||||
let edge_sides = |ks: &[usize]| -> (Vec<&mt::Face>, Vec<&mt::Face>) {
|
||||
let pick = |side: &[usize]| {
|
||||
let mut out: Vec<usize> = Vec::new();
|
||||
for k in ks {
|
||||
if let Some(fi) = side.get(*k)
|
||||
&& !out.contains(fi)
|
||||
{
|
||||
out.push(*fi);
|
||||
}
|
||||
}
|
||||
out.iter().filter_map(|i| faces.get(*i)).collect::<Vec<_>>()
|
||||
};
|
||||
(pick(&side1), pick(&side2))
|
||||
};
|
||||
// The chain's edges at vertex k.
|
||||
let vertex_edges = |k: usize| -> Vec<usize> {
|
||||
if closed {
|
||||
vec![(k + n - 1) % n, k % n]
|
||||
} else if k == 0 {
|
||||
vec![0]
|
||||
} else if k >= n {
|
||||
vec![n - 1]
|
||||
} else {
|
||||
vec![k - 1, k]
|
||||
}
|
||||
};
|
||||
// Convex or concave, from the first edge as its side-1 face runs it.
|
||||
let e0 = chain.first().ok_or_else(|| KernelError::Failed("chain".into()))?;
|
||||
let f1 = s1.first().ok_or_else(|| KernelError::Failed("side".into()))?;
|
||||
@@ -435,6 +559,8 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
let mut seams: Vec<Vec<(usize, mt::Edge)>> = Vec::with_capacity(nv);
|
||||
// At an open chain's ends: the end face (planar, square to the edge there).
|
||||
let mut end_faces: Vec<(usize, usize)> = Vec::new(); // (vertex index, face index)
|
||||
// Chain ends whose end face is not a plane square to the edge: (vertex, face, point, tangent).
|
||||
let mut slanted: Vec<(usize, usize, Vec3, Vec3)> = Vec::new();
|
||||
for (k, v) in verts.iter().enumerate() {
|
||||
let at_end = !closed && (k == 0 || k == nv - 1);
|
||||
let (k1, k2) = {
|
||||
@@ -491,19 +617,19 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
let here = seams.get(k).cloned().unwrap_or_default();
|
||||
let at_end = end_faces.iter().any(|(kk, _)| *kk == k);
|
||||
if at_end {
|
||||
// The end face must be square to the edge: the blend's end then lies in it.
|
||||
// A plane square to the edge holds the blend's end section. Any other end face: the
|
||||
// blend runs on past the end and is cut back where it meets that face.
|
||||
let (_, fi) = end_faces.iter().find(|(kk, _)| *kk == k).copied().unwrap_or_default();
|
||||
let g = faces.get(fi).ok_or_else(|| KernelError::Failed("end face".into()))?;
|
||||
let ng = match g.oriented_surface() {
|
||||
let square = match g.oriented_surface() {
|
||||
mt::Surface::Plane(pl) => {
|
||||
let m = pl.normal();
|
||||
Vec3::new(m.x, m.y, m.z).normalized()
|
||||
Vec3::new(m.x, m.y, m.z).normalized().is_some_and(|ng| ng.dot(t).abs() >= 1.0 - 1e-6)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
.ok_or_else(|| unsupported("the faces at the ends of the edges must be planar"))?;
|
||||
if ng.dot(t).abs() < 1.0 - 1e-6 {
|
||||
return Err(unsupported("the faces at the ends of the edges must be square to them"));
|
||||
_ => false,
|
||||
};
|
||||
if !square {
|
||||
slanted.push((k, fi, p, t));
|
||||
}
|
||||
}
|
||||
let extra = match here.first().cloned().filter(|_| !at_end) {
|
||||
@@ -516,12 +642,19 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
return Err(unsupported("a curved seam where the edge chain turns"));
|
||||
}
|
||||
let fs = if side == 1 { &s1 } else { &s2 };
|
||||
let nn =
|
||||
fs.iter().filter_map(|f| project(f, p, None)).map(|x| x.1).next().ok_or_else(|| KernelError::Failed("seam normal".into()))?;
|
||||
// The side's normal at the joint: from its face nearest the joint (any face of a
|
||||
// longer side is no guide here).
|
||||
let nn = fs
|
||||
.iter()
|
||||
.filter_map(|f| project(f, p, None))
|
||||
.min_by(|a, b| a.0.dist(p).total_cmp(&b.0.dist(p)))
|
||||
.map(|x| x.1)
|
||||
.ok_or_else(|| KernelError::Failed("seam normal".into()))?;
|
||||
Some((side, o, d.cross(nn).normalized().ok_or_else(|| KernelError::Failed("seam plane".into()))?))
|
||||
}
|
||||
};
|
||||
let s = solve(&s1, &s2, p, t, r, sigma, extra, tol * 100.0).ok_or_else(|| unsupported("the ball doesn't fit along the edge"))?;
|
||||
let (a1, a2) = edge_sides(&vertex_edges(k));
|
||||
let s = solve(&a1, &a2, p, t, r, sigma, extra, tol * 100.0).ok_or_else(|| unsupported("the ball doesn't fit along the edge"))?;
|
||||
// A seam on the other side too must pass through that side's contact.
|
||||
if let Some((side, seam)) = here.get(1).filter(|_| !at_end) {
|
||||
let q = if *side == 1 { s.q1 } else { s.q2 };
|
||||
@@ -532,6 +665,34 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
}
|
||||
vsec.push(s);
|
||||
}
|
||||
// Past a slanted end: sections of the same rolling ball beyond the end vertex (far to near
|
||||
// at the chain's start, near to far at its end), far enough to cross the end face.
|
||||
let mut beyond: Vec<(usize, Vec<Section>)> = Vec::new();
|
||||
for (k, fi, p, t) in &slanted {
|
||||
let g = faces.get(*fi).ok_or_else(|| KernelError::Failed("end face".into()))?;
|
||||
let at = vsec.get(*k).copied().ok_or_else(|| KernelError::Failed("section".into()))?;
|
||||
let (_, ng, _) = project(g, *p, None).ok_or_else(|| unsupported("the blend's end on its end face"))?;
|
||||
let cos = ng.dot(*t).abs();
|
||||
if cos < 0.15 {
|
||||
return Err(unsupported("an end face almost along the edge"));
|
||||
}
|
||||
let width = at.q1.dist(*p).max(at.q2.dist(*p)).max(at.c.dist(*p)) + r;
|
||||
let reach = 1.5 * width / cos;
|
||||
let out = if *k == 0 { -*t } else { *t };
|
||||
let (e1, e2) = edge_sides(&vertex_edges(*k));
|
||||
let mut list = Vec::with_capacity(EXTENSION);
|
||||
for j in 1..=EXTENSION {
|
||||
let anchor = *p + out * (reach * j as f64 / EXTENSION as f64);
|
||||
list.push(
|
||||
solve(&e1, &e2, anchor, *t, r, sigma, None, tol * 100.0)
|
||||
.ok_or_else(|| unsupported("the ball doesn't fit past the end of the edge"))?,
|
||||
);
|
||||
}
|
||||
if *k == 0 {
|
||||
list.reverse();
|
||||
}
|
||||
beyond.push((*k, list));
|
||||
}
|
||||
// Uniform refinement preserves a common spacing across neighbouring patches. Work is
|
||||
// capped: a difficult strip returns an error instead of accepting an inaccurate fit.
|
||||
let fit_tolerance = (size * 1e-5).clamp(1e-5, 0.001);
|
||||
@@ -554,7 +715,8 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
let (p, t) = tangent_at(edge, along).ok_or_else(|| KernelError::Failed("tangent".into()))?;
|
||||
let (start, end) = offsets.get(k).copied().ok_or_else(|| KernelError::Failed("section offset".into()))?;
|
||||
let anchor = p + t * (start * (1.0 - along) + end * along);
|
||||
solve(&s1, &s2, anchor, t, r, sigma, None, tol * 100.0).ok_or_else(|| unsupported("the ball doesn't fit along the edge"))
|
||||
let (a1, a2) = edge_sides(&[k]);
|
||||
solve(&a1, &a2, anchor, t, r, sigma, None, tol * 100.0).ok_or_else(|| unsupported("the ball doesn't fit along the edge"))
|
||||
};
|
||||
let mut samples = SAMPLES.min(max_samples);
|
||||
let (esec, fits) = loop {
|
||||
@@ -568,6 +730,13 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
list.push(section_at(k, e, j as f64 / (samples - 1) as f64)?);
|
||||
}
|
||||
list.push(*vsec.get((k + 1) % nv).ok_or_else(|| KernelError::Failed("section".into()))?);
|
||||
for (kv, ext) in &beyond {
|
||||
if *kv == k && k == 0 {
|
||||
list.splice(0..0, ext.iter().copied());
|
||||
} else if *kv == k + 1 && k + 1 == nv - 1 {
|
||||
list.extend(ext.iter().copied());
|
||||
}
|
||||
}
|
||||
guides.push(Guide::new(list)?);
|
||||
}
|
||||
// Intersection curves inherit their leader's piecewise parameter speed. Fitting
|
||||
@@ -583,12 +752,41 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
return guide.sections.last().copied().ok_or_else(|| KernelError::Failed("section".into()));
|
||||
}
|
||||
let (anchor, tangent) = guide.anchor(along).ok_or_else(|| unsupported("degenerate rolling-ball spine"))?;
|
||||
solve(&s1, &s2, anchor, tangent, r, sigma, None, tol * 100.0).ok_or_else(|| unsupported("the ball doesn't fit along the resampled spine"))
|
||||
let (a1, a2) = edge_sides(&[k]);
|
||||
solve(&a1, &a2, anchor, tangent, r, sigma, None, tol * 100.0).ok_or_else(|| unsupported("the ball doesn't fit along the resampled spine"))
|
||||
};
|
||||
let esec: Vec<Vec<Section>> = (0..n)
|
||||
.map(|k| (0..samples).map(|j| uniform_section(k, j as f64 / (samples - 1) as f64)).collect::<Result<Vec<_>>>())
|
||||
.collect::<Result<_>>()?;
|
||||
let fits = crate::guard("fit curve blend", || (0..n).map(|k| fit_sections(&esec, k, closed, round)).collect::<Result<Vec<_>>>())?;
|
||||
// Each strip's neighbouring sections at its own spacing (edges of different lengths
|
||||
// have differently spaced samples, which makes a uniform interpolation overshoot).
|
||||
let length = |k: usize| guides.get(k).and_then(|g| g.lengths.last().copied()).unwrap_or(0.0);
|
||||
let mut fits = Vec::with_capacity(n);
|
||||
for k in 0..n {
|
||||
let h = length(k) / (samples - 1) as f64;
|
||||
let spaced = |nb: usize, from_end: bool| -> Result<Option<Vec<Section>>> {
|
||||
let l = length(nb);
|
||||
if !(l > 2.0 * h && h > 0.0) {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut out = Vec::with_capacity(2);
|
||||
for j in [1.0, 2.0] {
|
||||
out.push(uniform_section(nb, if from_end { 1.0 - j * h / l } else { j * h / l })?);
|
||||
}
|
||||
if from_end {
|
||||
out.reverse();
|
||||
}
|
||||
Ok(Some(out))
|
||||
};
|
||||
let before = if closed || k > 0 { spaced((k + n - 1) % n, true)? } else { Some(Vec::new()) };
|
||||
let after = if closed || k + 1 < n { spaced((k + 1) % n, false)? } else { Some(Vec::new()) };
|
||||
let list = esec.get(k).ok_or_else(|| KernelError::Failed("section".into()))?;
|
||||
let fit = match (before, after) {
|
||||
(Some(b), Some(a)) => crate::guard("fit curve blend", || fit_strip(list, &b, &a, round))?,
|
||||
_ => crate::guard("fit curve blend", || fit_sections(&esec, k, closed, round))?,
|
||||
};
|
||||
fits.push(fit);
|
||||
}
|
||||
let mut accurate = true;
|
||||
'edges: for k in 0..n {
|
||||
let fit = fits.get(k).ok_or_else(|| KernelError::Failed("curve blend fit".into()))?;
|
||||
@@ -611,11 +809,37 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
}
|
||||
samples = (2 * samples - 1).min(max_samples);
|
||||
};
|
||||
// Where each extended blend meets its slanted end face: (vertex, edge, cut parameters of
|
||||
// the two contacts, the end curve from contact 1 to contact 2).
|
||||
let mut caps: Vec<(usize, usize, f64, f64, mt::Curve)> = Vec::new();
|
||||
for (k, fi, _, _) in &slanted {
|
||||
let ke = if *k == 0 { 0 } else { n - 1 };
|
||||
let fit = fits.get(ke).ok_or_else(|| KernelError::Failed("curve blend fit".into()))?;
|
||||
let g = faces.get(*fi).ok_or_else(|| KernelError::Failed("end face".into()))?;
|
||||
let at = vsec.get(*k).copied().ok_or_else(|| KernelError::Failed("section".into()))?;
|
||||
let cap = crate::guard("curve blend end", || end_cap(fit, g, &at, round, fit_tolerance))?;
|
||||
caps.push((*k, ke, cap.0, cap.1, cap.2));
|
||||
}
|
||||
// Both ends of one edge cut back: the contacts must keep a piece between them.
|
||||
if let [(_, 0, s1, s2, _), (_, 0, e1, e2, _)] = &caps[..]
|
||||
&& n == 1
|
||||
&& !(s1 + 1e-6 < *e1 && s2 + 1e-6 < *e2)
|
||||
{
|
||||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
crate::guard("curve blend", || {
|
||||
let fail = |m: &str| KernelError::Failed(format!("curve blend: {m}"));
|
||||
// Vertices and arcs at the chain's vertices.
|
||||
let v1: Vec<mt::Vertex> = vsec.iter().map(|s| builder::vertex(p3(s.q1))).collect();
|
||||
let v2: Vec<mt::Vertex> = vsec.iter().map(|s| builder::vertex(p3(s.q2))).collect();
|
||||
let mut v1: Vec<mt::Vertex> = vsec.iter().map(|s| builder::vertex(p3(s.q1))).collect();
|
||||
let mut v2: Vec<mt::Vertex> = vsec.iter().map(|s| builder::vertex(p3(s.q2))).collect();
|
||||
for (k, _, _, _, curve) in &caps {
|
||||
use mt::{BoundedCurve, ParametricCurve};
|
||||
let (lo, hi) = curve.range_tuple();
|
||||
if let (Some(a), Some(b)) = (v1.get_mut(*k), v2.get_mut(*k)) {
|
||||
*a = builder::vertex(curve.subs(lo));
|
||||
*b = builder::vertex(curve.subs(hi));
|
||||
}
|
||||
}
|
||||
let mut arcs = Vec::with_capacity(nv);
|
||||
for (k, s) in vsec.iter().enumerate() {
|
||||
let (a, b) = (v1.get(k).ok_or_else(|| fail("vertex"))?, v2.get(k).ok_or_else(|| fail("vertex"))?);
|
||||
@@ -625,6 +849,10 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
} else {
|
||||
builder::line(a, b)
|
||||
};
|
||||
let e = match caps.iter().find(|c| c.0 == k) {
|
||||
Some((_, _, _, _, curve)) => mt::Edge::try_new(a, b, curve.clone()).map_err(|e| fail(&e.to_string()))?,
|
||||
None => e,
|
||||
};
|
||||
arcs.push(e);
|
||||
}
|
||||
// Contact curves and blend faces per edge.
|
||||
@@ -637,8 +865,24 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
|
||||
let (Some(a1), Some(b1), Some(a2), Some(b2)) = (a1, b1, a2, b2) else { return Err(fail("vertex")) };
|
||||
let fit = fits.get(k).ok_or_else(|| fail("fit"))?;
|
||||
let [cv1, cv2] = &fit.contacts;
|
||||
let e1 = mt::Edge::new(a1, b1, cv1.clone());
|
||||
let e2 = mt::Edge::new(a2, b2, cv2.clone());
|
||||
// Contacts past a slanted end stop at the end curve.
|
||||
let (mut cv1, mut cv2) = (cv1.clone(), cv2.clone());
|
||||
for (kv, _, t1, t2, _) in caps.iter().filter(|c| c.1 == k) {
|
||||
use mt::Cut;
|
||||
if *kv == k + 1 {
|
||||
let _ = cv1.cut(*t1);
|
||||
let _ = cv2.cut(*t2);
|
||||
}
|
||||
}
|
||||
for (kv, _, t1, t2, _) in caps.iter().filter(|c| c.1 == k) {
|
||||
use mt::Cut;
|
||||
if *kv == k && k == 0 {
|
||||
cv1 = cv1.cut(*t1);
|
||||
cv2 = cv2.cut(*t2);
|
||||
}
|
||||
}
|
||||
let e1 = mt::Edge::new(a1, b1, cv1);
|
||||
let e2 = mt::Edge::new(a2, b2, cv2);
|
||||
let mut surface = fit.surface.clone();
|
||||
// Outward: toward the ball centre on a concave edge, away on a convex one.
|
||||
{
|
||||
@@ -678,6 +922,30 @@ 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 {
|
||||
// At a slanted end the blend may stop past the seam's old end: the seam then runs
|
||||
// on along its line or circle, within its planar face.
|
||||
let one_end = match (&a, &b) {
|
||||
(Some(nv), None) => Some((seam.front().clone(), nv.clone())),
|
||||
(None, Some(nv)) => Some((seam.back().clone(), nv.clone())),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((old, nv)) = one_end
|
||||
&& slanted.iter().any(|(k, ..)| verts.get(*k) == Some(&old))
|
||||
{
|
||||
let on_tol = (size * 1e-5).max(1e-6);
|
||||
let q = from_p3(nv.point());
|
||||
if crate::blend::on_edge(&seam, q, on_tol).is_none() {
|
||||
let fits = faces
|
||||
.iter()
|
||||
.filter(|f| f.edge_iter().any(|e| e.id() == id))
|
||||
.any(|f| crate::blend::plane_normal(f).is_some() && crate::blend::extension_fits(f, from_p3(old.point()), q, tol));
|
||||
if !fits || !crate::blend::on_extension(&seam, q, on_tol) {
|
||||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||||
}
|
||||
}
|
||||
seam_subst.insert(id, crate::blend::retarget(&seam, &old, &nv, on_tol)?);
|
||||
continue;
|
||||
}
|
||||
if !matches!(seam.curve(), mt::Curve::Line(_)) {
|
||||
seam_subst.insert(id, trim_curved_seam(&seam, a, b, size)?);
|
||||
continue;
|
||||
|
||||
+401
-401
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user