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:
1 parent
45f92277cb
commit
a2c7396160
10 files changed
+1364
-54
No files matched your search
+300
-29
@@ -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()
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user