Fillets and chamfers where edges meet at corners on a curved wall or a knife-edge tip (#851) (#935)

Rolling-ball blends now handle knife edges, where the two faces have opposite normals; an edge that runs out along an end face tangent to one side; and rounds that end in an earlier round on curved faces, picked together or one after another. The three edges at a vertex of smooth faces, rounded to one radius, end in a piece of sphere. When the given order of up to four edges is refused, or leaves a face boundary off its surface, other orders are tried.

Closes #851.

Co-authored-by: lasselian <22126420+lasselian@users.noreply.github.com>
This commit is contained in:
Lasse Lianandlasselian authored and GitHub committed 2026-10-13 23:21:47 -04:00
1 parent 45f92277cb
commit a2c7396160
10 files changed
+1364 -54

No files matched your search

+300 -29
View File
@@ -26,6 +26,15 @@ 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;
/// `1 + n1·n2` below this: a knife edge, where the faces' normals are opposite and the ball's
/// centre is found by sliding along the faces rather than from their normals.
const KNIFE: f64 = 1e-4;
/// An end face this close to square-on-to-its-sides' normal counts as tangent to a side.
const TANGENT_END: f64 = 1e-6;
/// Times the run past a tangent end is halved when the ball does not fit along it.
const TANGENT_RETRIES: usize = 8;
/// Slides of the ball's centre along a knife edge's faces, each 1.6 times the last.
const KNIFE_STEPS: usize = 40;
struct Fit {
surface: mt::Surface,
@@ -177,7 +186,7 @@ fn section_fits(fit: &Fit, section: &Section, along: f64, round: bool, sigma: f6
/// 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)> {
fn end_cap(fit: &Fit, g: &mt::Face, at: &Section, seams: [Option<&mt::Edge>; 2], 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());
@@ -235,6 +244,7 @@ fn end_cap(fit: &Fit, g: &mt::Face, at: &Section, round: bool, tolerance: f64) -
vs.push(v);
pts.push(from_p3(surf.subs(i as f64 / m as f64, v)));
}
snap_to_seams(&mut pts, seams, tolerance);
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)));
@@ -256,6 +266,148 @@ fn end_cap(fit: &Fit, g: &mt::Face, at: &Section, round: bool, tolerance: f64) -
Err(fail())
}
/// The ends of an end curve (contact 1 to contact 2) that land within `tolerance` of an end of
/// their side's seam are that point exactly: the blend and the faces there meet at one vertex.
fn snap_to_seams(pts: &mut [Vec3], seams: [Option<&mt::Edge>; 2], tolerance: f64) {
let snap = |p: &mut Vec3, seam: Option<&mt::Edge>| {
for q in seam.into_iter().flat_map(|e| [from_p3(e.front().point()), from_p3(e.back().point())]) {
if p.dist(q) <= tolerance {
*p = q;
}
}
};
if let Some(first) = pts.first_mut() {
snap(first, seams[0]);
}
if let Some(last) = pts.last_mut() {
snap(last, seams[1]);
}
}
/// Where a blend strip run on past a chain end meets an end face that lies along the edge and is
/// tangent to one side there (a wall running out into the face, an earlier round that touches a
/// side along its contact): the end curve from contact 1 to contact 2, the same result as
/// [`end_cap`]. The strip touches the face along the tangent side's contact, so a Newton solve
/// for the crossing is singular there.
///
/// Both ends of the curve are where a contact meets a seam, which is a plain crossing: the
/// tangent side's contact leaves the end face where it crosses the seam of that side with the
/// face, the other contact where it crosses the other side's seam. Between those stations each
/// cross-section crosses the face once, a simple root. Either station may lie before or after
/// the vertex's section: the curve runs inwards along the edge or out along the extension.
fn tangent_cap(fit: &Fit, g: &mt::Face, tangent_side: u8, seams: [Option<&mt::Edge>; 2], tolerance: f64) -> Result<(f64, f64, mt::Curve)> {
use mt::{BoundedCurve, ParametricCurve, ParametricSurface, SearchNearestParameter};
let fail = || unsupported("the blend's end on its end face");
let surf = &fit.surface;
let (lo, hi) = (fit.range.0.min(fit.range.1), fit.range.0.max(fit.range.1));
let (u_t, u_o, seam_t, seam_o) = if tangent_side == 1 { (0.0, 1.0, seams[0], seams[1]) } else { (1.0, 0.0, seams[1], seams[0]) };
let (seam_t, seam_o) = (seam_t.ok_or_else(fail)?, seam_o.ok_or_else(fail)?);
// The station where the strip's contact `u` crosses `seam`.
let crossing = |u: f64, seam: &mt::Edge| -> Option<f64> {
let curve = seam.curve();
let (a, b) = curve.range_tuple();
let polyline: Vec<Vec3> = (0..=1024).map(|i| from_p3(curve.subs(a + (b - a) * f64::from(i) / 1024.0))).collect();
let dist = |v: f64| -> f64 {
let x = from_p3(surf.subs(u, v));
polyline.windows(2).map(|w| x.dist_to_segment(w[0], w[1])).fold(f64::INFINITY, f64::min)
};
let n = 400;
let step = (hi - lo) / f64::from(n);
let (mut best, mut best_d) = (lo, f64::INFINITY);
for i in 0..=n {
let v = lo + step * f64::from(i);
let d = dist(v);
if d < best_d {
(best, best_d) = (v, d);
}
}
let (mut x0, mut x1) = ((best - step).max(lo), (best + step).min(hi));
let phi = 0.618_033_988_749_894_9;
for _ in 0..80 {
let (c, d) = (x1 - phi * (x1 - x0), x0 + phi * (x1 - x0));
if dist(c) < dist(d) {
x1 = d;
} else {
x0 = c;
}
}
let v = 0.5 * (x0 + x1);
(dist(v) <= tolerance).then_some(v)
};
let (rt, ra) = (crossing(u_t, seam_t), crossing(u_o, seam_o));
let v_t = rt.ok_or_else(fail)?;
let v_a = ra.ok_or_else(|| too_large("the blend is larger than the neighbouring faces"))?;
let inside = |t: f64| t > lo + 1e-6 && t < hi - 1e-6;
if !(inside(v_a) && inside(v_t)) || (v_a - v_t).abs() < 1e-9 {
return Err(too_large("the blend is larger than the neighbouring faces"));
}
// How far the strip's point (u, v) is outside the end face (negative: inside the body).
let outside = |u: f64, v: f64| -> Option<f64> {
let x = from_p3(surf.subs(u, v));
let (q, n, _) = project(g, x, None)?;
Some((x - q).dot(n))
};
// Root of `f` between `lo` and `hi`, `f(lo)` and `f(hi)` of different sign.
let bisect = |f: &dyn Fn(f64) -> Option<f64>, mut x0: f64, mut x1: f64| -> Option<f64> {
let f0 = f(x0)?;
for _ in 0..60 {
let mid = 0.5 * (x0 + x1);
let fm = f(mid)?;
if fm.abs() <= tolerance * 1e-6 {
return Some(mid);
}
if fm.is_sign_negative() == f0.is_sign_negative() {
x0 = mid;
} else {
x1 = mid;
}
}
Some(0.5 * (x0 + x1))
};
for m in [16usize, 32, 64, 128] {
// The curve at stations v_i from the crossing contact to the tangent one, as (u, v, point).
let mut stations: Vec<(f64, f64, Vec3)> = Vec::with_capacity(m + 1);
for i in 0..=m {
// Clustered at both ends, where the curve can meet a contact tangentially (a square
// root in the station) and a uniform spacing would fit it poorly.
let v = v_a + (v_t - v_a) * 0.5 * (1.0 - (std::f64::consts::PI * i as f64 / m as f64).cos());
let u = if i == 0 {
u_o
} else if i == m {
u_t
} else {
let at_t = outside(u_t, v).ok_or_else(fail)?;
let at_o = outside(u_o, v).ok_or_else(fail)?;
// No crossing where the contacts are on one side: the curve is at the tangent one.
if at_t * at_o >= 0.0 { u_t } else { bisect(&|x| outside(x, v), u_o, u_t).ok_or_else(fail)? }
};
stations.push((u, v, from_p3(surf.subs(u, v))));
}
let mut pts: Vec<Vec3> = stations.iter().map(|x| x.2).collect();
// The curve runs from contact 1 to contact 2.
if u_o == 1.0 {
pts.reverse();
}
snap_to_seams(&mut pts, seams, tolerance);
let bs = crate::build::interpolate_cubic(&pts).ok_or_else(fail)?;
let gs = g.surface();
let fits = stations.windows(2).enumerate().all(|(i, w)| {
let (Some(a), Some(b)) = (w.first(), w.get(1)) else { return false };
let t = if u_o == 1.0 { 1.0 - (i as f64 + 0.5) / m as f64 } else { (i as f64 + 0.5) / m as f64 };
let q = p3(from_p3(bs.subs(t)));
let hint = (0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
let on_g = gs.search_nearest_parameter(q, None, 100).is_some_and(|(x, y)| from_p3(gs.subs(x, y)).dist(from_p3(q)) <= tolerance);
let on_s = surf.search_nearest_parameter(q, Some(hint), 100).is_some_and(|(x, y)| from_p3(surf.subs(x, y)).dist(from_p3(q)) <= tolerance);
on_g && on_s
});
if fits {
let (t1, t2) = if u_o == 0.0 { (v_a, v_t) } else { (v_t, v_a) };
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 {
@@ -265,7 +417,7 @@ struct Section {
}
/// Nearest point of a face's surface and the outward normal there.
fn project(f: &mt::Face, p: Vec3, hint: Option<(f64, f64)>) -> Option<(Vec3, Vec3, (f64, f64))> {
pub(crate) fn project(f: &mt::Face, p: Vec3, hint: Option<(f64, f64)>) -> Option<(Vec3, Vec3, (f64, f64))> {
use mt::{ParametricSurface, ParametricSurface3D, SearchNearestParameter};
let s = f.oriented_surface();
let uv = s.search_nearest_parameter(p3(p), hint, 100).or_else(|| s.search_nearest_parameter(p3(p), None, 100))?;
@@ -294,8 +446,20 @@ fn in_range(f: &mt::Face, uv: (f64, f64)) -> bool {
/// 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.
/// `cusp` is ±1 at a knife edge (the faces' normals opposite: both leave the edge the same way,
/// along `cusp · n1 × t`, and the ball lies where the cusp has opened to its width), else 0.
#[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> {
fn solve(
f1: &[&mt::Face],
f2: &[&mt::Face],
p: Vec3,
t: Vec3,
r: f64,
sigma: f64,
cusp: f64,
extra: Option<(usize, Vec3, Vec3)>,
tol: f64,
) -> Option<Section> {
// 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
@@ -320,10 +484,47 @@ fn solve(f1: &[&mt::Face], f2: &[&mt::Face], p: Vec3, t: Vec3, r: f64, sigma: f6
let (q2, n2) = near(f2, p)?;
let _ = (q1, q2);
let den = 1.0 + n1.dot(n2);
if den.abs() < 1e-9 {
return None;
}
let mut c = p + (n1 + n2) * (sigma * r / den);
let mut c = if cusp != 0.0 && den.abs() < KNIFE {
// The normals are (nearly) opposite: `(n1 + n2) / den` has no direction. The centre is r
// inside face 1 and slides along it, away from the edge, until it is r from face 2 too.
let w = n1.cross(t).normalized()? * cusp;
let centre = |s: f64| -> Option<Vec3> {
let (qa, na) = near(f1, p + w * s)?;
Some(qa + na * (sigma * r))
};
let gap = |s: f64| -> Option<f64> {
let c = centre(s)?;
let (q2, n2) = near(f2, c)?;
Some((c - q2).dot(n2) - sigma * r)
};
let (mut lo, mut g_lo) = (0.0, gap(0.0)?);
let mut hi = None;
let mut s = r * 0.05;
for _ in 0..KNIFE_STEPS {
let Some(g) = gap(s) else { break };
if g == 0.0 || g.is_sign_negative() != g_lo.is_sign_negative() {
hi = Some(s);
break;
}
(lo, g_lo) = (s, g);
s *= 1.6;
}
let mut hi = hi?;
for _ in 0..60 {
let mid = 0.5 * (lo + hi);
match gap(mid) {
Some(g) if g.is_sign_negative() == g_lo.is_sign_negative() => lo = mid,
Some(_) => hi = mid,
None => break,
}
}
centre(0.5 * (lo + hi))?
} else {
if den.abs() < 1e-9 {
return None;
}
p + (n1 + n2) * (sigma * r / den)
};
for _ in 0..60 {
let (q1, n1) = near(f1, c)?;
let (q2, n2) = near(f2, c)?;
@@ -549,7 +750,23 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
(Some(x), Some(y)) => (x.1, y.1),
_ => return Err(KernelError::Failed("blend: normals".into())),
};
let convex = t1.dot(n1.cross(n2)) > 0.0;
// A knife edge (opposite normals): both faces leave the edge along `w`; the second one lies
// below the first (against its outward normal) when the cusp is material, above when it is
// a gap. The cross product of the normals, which tells the other edges apart, is zero here.
let knife = 1.0 + n1.dot(n2) < KNIFE;
let (convex, cusp) = if knife {
let w = n1.cross(t1).normalized().ok_or_else(|| KernelError::Failed("blend: cusp direction".into()))?;
// Second order: from the point of face 1 a little way along `w`, face 2's nearest point.
let probe = project(f1, pm + w * r, None)
.and_then(|(qa, na, _)| s2.first().and_then(|f| project(f, qa, None)).map(|(qb, _, _)| (qb - qa).dot(na)))
.filter(|depth| depth.abs() > size * 1e-9)
.ok_or_else(|| unsupported("a knife edge whose faces lie flat on each other"))?;
// Whether the chain runs along the edge as face 1 uses it decides which way `w` points
// from the chain's tangent.
(probe < 0.0, if in_f1.front() == e0.front() { 1.0 } else { -1.0 })
} else {
(t1.dot(n1.cross(n2)) > 0.0, 0.0)
};
let sigma = if convex { -1.0 } else { 1.0 };
if n1.dot(n2) > 1.0 - 1e-6 {
return Err(unsupported("blending a smooth edge"));
@@ -559,8 +776,9 @@ 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();
// Chain ends whose end face is not a plane square to the edge: (vertex, face, point, tangent,
// the side (1 or 2) the end face is tangent to when it lies along the edge there).
let mut slanted: Vec<(usize, usize, Vec3, Vec3, Option<u8>)> = Vec::new();
for (k, v) in verts.iter().enumerate() {
let at_end = !closed && (k == 0 || k == nv - 1);
let (k1, k2) = {
@@ -629,7 +847,7 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
_ => false,
};
if !square {
slanted.push((k, fi, p, t));
slanted.push((k, fi, p, t, None));
}
}
let extra = match here.first().cloned().filter(|_| !at_end) {
@@ -654,7 +872,7 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
}
};
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"))?;
let s = solve(&a1, &a2, p, t, r, sigma, cusp, 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 };
@@ -668,25 +886,46 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
// 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 {
for (k, fi, p, t, tangent) in slanted.iter_mut() {
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 {
// An end face along the edge and tangent to one side there (a wall that runs out into
// the face): the blend meets it along a curve that touches that side's contact at the
// vertex. Any other end face that close to the edge is refused.
let (e1, e2) = edge_sides(&vertex_edges(*k));
*tangent = if cos < TANGENT_END {
[(1u8, &e1), (2u8, &e2)].into_iter().find_map(|(side, faces)| {
faces.iter().filter_map(|f| project(f, *p, None)).any(|(_, n, _)| n.dot(ng).abs() > 1.0 - TANGENT_END).then_some(side)
})
} else {
None
};
if cos < 0.15 && tangent.is_none() {
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 reach = 1.5 * width / if tangent.is_some() { 1.0 } else { cos };
let out = if *k == 0 { -*t } else { *t };
let (e1, e2) = edge_sides(&vertex_edges(*k));
// Past a tangent end the straight run along the tangent leaves the curved spine: where
// the ball no longer fits, the run is shortened (it only gives the fit its context).
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"))?,
);
let mut reach = reach;
for attempt in 0..(if tangent.is_some() { TANGENT_RETRIES } else { 1 }) {
list.clear();
let sections = (1..=EXTENSION).map(|j| {
let anchor = *p + out * (reach * j as f64 / EXTENSION as f64);
solve(&e1, &e2, anchor, *t, r, sigma, cusp, None, tol * 100.0)
});
if let Some(all) = sections.collect::<Option<Vec<_>>>() {
list = all;
break;
}
if attempt + 1 == TANGENT_RETRIES || tangent.is_none() {
return Err(unsupported("the ball doesn't fit past the end of the edge"));
}
reach *= 0.5;
}
if *k == 0 {
list.reverse();
@@ -716,7 +955,7 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
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);
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"))
solve(&a1, &a2, anchor, t, r, sigma, cusp, 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 {
@@ -753,7 +992,8 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
}
let (anchor, tangent) = guide.anchor(along).ok_or_else(|| unsupported("degenerate rolling-ball 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"))
solve(&a1, &a2, anchor, tangent, r, sigma, cusp, 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<_>>>())
@@ -812,12 +1052,17 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
// 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 {
for (k, fi, _, _, tangent) 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))?;
let seam = |s: u8| seams.get(*k).and_then(|l| l.iter().find(|(x, _)| *x as u8 == s)).map(|(_, e)| e);
let both = [seam(1), seam(2)];
let cap = crate::guard("curve blend end", || match tangent {
Some(side) => tangent_cap(fit, g, *side, both, fit_tolerance),
None => end_cap(fit, g, &at, both, 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.
@@ -840,6 +1085,23 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
*b = builder::vertex(curve.subs(hi));
}
}
// A chain end (a cap, or the section at a square end) on the far end of its seam uses that
// seam up: that vertex is the end.
let mut consumed: HashMap<mt::EdgeID, mt::Vertex> = HashMap::new();
for (k, _) in &end_faces {
for (side, seam) in seams.get(*k).into_iter().flatten() {
let Some(old) = verts.get(*k) else { continue };
let far = if seam.front() == old { seam.back() } else { seam.front() };
let slot = if *side == 1 { v1.get_mut(*k) } else { v2.get_mut(*k) };
if let Some(nv) = slot
&& far != old
&& from_p3(far.point()).dist(from_p3(nv.point())) <= fit_tolerance
{
*nv = far.clone();
consumed.insert(seam.id(), far.clone());
}
}
}
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"))?);
@@ -922,6 +1184,9 @@ 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 consumed.contains_key(&id) {
continue;
}
// 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) {
@@ -988,13 +1253,17 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
}
None => e.clone(),
};
// Arriving at an old corner: continue along that end's arc.
// Arriving at an old corner: continue along that end's arc. A seam the
// blend used up is left out, and the arc starts at its far end.
let arrives = corners.iter().copied().find(|k| verts.get(*k).is_some_and(|v| *e.back() == *v));
es.push(ne.clone());
let used_up = consumed.contains_key(&e.id());
if !used_up {
es.push(ne.clone());
}
if let Some(k) = arrives {
let arc = arcs.get(k).ok_or_else(|| fail("arc"))?;
let (a1, a2) = (v1.get(k).ok_or_else(|| fail("vertex"))?, v2.get(k).ok_or_else(|| fail("vertex"))?);
let to = ne.back();
let to = if used_up { e.front() } else { ne.back() };
let corner = if to == a1 {
arc.clone()
} else if to == a2 {
@@ -1029,6 +1298,8 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
if e.front() == chain_e.front() { base.clone() } else { base.inverse() }
} else if let Some(s) = seam_subst.get(&e.id()) {
if e.front() == e.absolute_front() { s.clone() } else { s.inverse() }
} else if consumed.contains_key(&e.id()) {
continue;
} else {
e.clone()
};