A curved chamfer chain with straight planar ends can end across two earlier planar bevel faces, shortening their shared seam to where the new chamfer plane crosses it; picks on one tangent chain are applied once. Fixes #1303.
1869 lines
94 KiB
Rust
1869 lines
94 KiB
Rust
//! Rolling-ball blends along a closed chain of smooth edges between two smooth groups of faces
|
||
//! of any surfaces (a branch pipe meeting a main pipe, a boss on a cylinder). A ball of the
|
||
//! blend's radius rolls along the edge touching both sides; its centre traces the spine and its
|
||
//! contacts the curves where the blend meets each side. The blend face is a rational B-spline
|
||
//! through the circular cross-sections (exact arcs at the samples), the side faces are trimmed
|
||
//! back to the contact curves, and seams that met the edge (lines, or arcs of an end face) are
|
||
//! shortened to the contacts.
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use solidcraft_geom::Vec3;
|
||
use truck_modeling::{self as mt, builder};
|
||
|
||
use crate::blend::too_large;
|
||
use crate::body::{Solid, from_p3, p3};
|
||
use crate::record::Rebuild;
|
||
use crate::{KernelError, Result};
|
||
|
||
fn unsupported(msg: &str) -> KernelError {
|
||
KernelError::Failed(format!("not supported yet: {msg}"))
|
||
}
|
||
|
||
/// Samples per edge for the contact curves and the blend surface.
|
||
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;
|
||
/// `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 (and the run
|
||
/// past a curved end is shortened when its sections wrap round).
|
||
const TANGENT_RETRIES: usize = 8;
|
||
/// The longest step between the centres of neighbouring sections past an open chain's end, as a
|
||
/// multiple of the step along the end tangent: longer, and the sections have wrapped round a
|
||
/// curved edge's ring of centres.
|
||
const MAX_STEP_STRETCH: f64 = 3.0;
|
||
/// 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,
|
||
contacts: [mt::Curve; 2],
|
||
/// Parameter range belonging to this edge (the fit includes neighbouring sections).
|
||
range: (f64, f64),
|
||
}
|
||
|
||
struct Guide {
|
||
sections: Vec<Section>,
|
||
lengths: Vec<f64>,
|
||
}
|
||
|
||
impl Guide {
|
||
fn new(sections: Vec<Section>) -> Result<Self> {
|
||
let mut lengths = vec![0.0];
|
||
let mut total = 0.0;
|
||
for pair in sections.windows(2) {
|
||
let (Some(a), Some(b)) = (pair.first(), pair.get(1)) else { continue };
|
||
total += a.c.dist(b.c);
|
||
lengths.push(total);
|
||
}
|
||
if !total.is_finite() || total <= 1e-12 {
|
||
return Err(unsupported("degenerate rolling-ball spine"));
|
||
}
|
||
Ok(Self { sections, lengths })
|
||
}
|
||
|
||
fn anchor(&self, along: f64) -> Option<(Vec3, Vec3)> {
|
||
if self.lengths.len() < 2 || !along.is_finite() || !(0.0..=1.0).contains(&along) {
|
||
return None;
|
||
}
|
||
let distance = self.lengths.last()? * along;
|
||
let hi = self.lengths.partition_point(|length| *length < distance).clamp(1, self.lengths.len().checked_sub(1)?);
|
||
let (a, b) = (self.sections.get(hi - 1)?, self.sections.get(hi)?);
|
||
let (lo, hi_length) = (self.lengths.get(hi - 1)?, self.lengths.get(hi)?);
|
||
let span = hi_length - lo;
|
||
if span <= 1e-12 {
|
||
return None;
|
||
}
|
||
let fraction = (distance - lo) / span;
|
||
Some((a.c + (b.c - a.c) * fraction, (b.c - a.c).normalized()?))
|
||
}
|
||
}
|
||
|
||
/// 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> {
|
||
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 mut before = Vec::new();
|
||
if closed || k > 0 {
|
||
let prev = sections.get((k + n - 1) % n).ok_or_else(|| fail("section"))?;
|
||
before.extend(prev.iter().rev().skip(1).take(2).rev().copied());
|
||
}
|
||
let mut after = Vec::new();
|
||
if closed || k + 1 < n {
|
||
let next = sections.get((k + 1) % n).ok_or_else(|| fail("section"))?;
|
||
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;
|
||
for j in 0..3 {
|
||
let xyz: Vec<Vec3> = ctrl.iter().filter_map(|c| c.get(j)).map(|c| Vec3::new(c.x, c.y, c.z)).collect();
|
||
let ww: Vec<Vec3> = ctrl.iter().filter_map(|c| c.get(j)).map(|c| Vec3::new(c.w, 0.0, 0.0)).collect();
|
||
let bx = crate::build::interpolate_cubic(&xyz).ok_or_else(|| fail("blend surface"))?;
|
||
let bw = crate::build::interpolate_cubic(&ww).ok_or_else(|| fail("blend surface"))?;
|
||
vknots = Some(bx.knot_vec().clone());
|
||
rows.push(bx.control_points().iter().zip(bw.control_points()).map(|(p, w)| mt::Vector4::new(p.x, p.y, p.z, w.x)).collect::<Vec<_>>());
|
||
}
|
||
let vk = vknots.ok_or_else(|| fail("knots"))?;
|
||
let den = ext.len().saturating_sub(1) as f64;
|
||
if den <= 0.0 {
|
||
return Err(fail("empty sections"));
|
||
}
|
||
let range = (before as f64 / den, (before + list.len().saturating_sub(1)) as f64 / den);
|
||
let contact = |row: usize| -> Result<mt::Curve> {
|
||
let points = rows.get(row).cloned().ok_or_else(|| fail("contact row"))?;
|
||
let bs = mt::BSplineCurve::try_new(vk.clone(), points).map_err(|e| fail(&e.to_string()))?;
|
||
let mut curve = mt::NurbsCurve::new(bs);
|
||
if range.0 > 0.0 {
|
||
curve = curve.cut(range.0);
|
||
}
|
||
if range.1 < 1.0 {
|
||
let _ = curve.cut(range.1);
|
||
}
|
||
Ok(mt::Curve::NurbsCurve(curve))
|
||
};
|
||
let contacts = [contact(0)?, contact(2)?];
|
||
let bs = mt::BSplineSurface::try_new((mt::KnotVec::bezier_knot(2), vk), rows).map_err(|e| fail(&e.to_string()))?;
|
||
Ok(Fit { surface: mt::Surface::NurbsSurface(mt::NurbsSurface::new(bs)), contacts, range })
|
||
}
|
||
|
||
/// Compare the fitted strip with a solved circular section between interpolation nodes.
|
||
/// This checks the supporting contacts as well as the middle of the fillet band.
|
||
fn section_fits(fit: &Fit, section: &Section, along: f64, round: bool, sigma: f64, tolerance: f64) -> bool {
|
||
use mt::{ParametricSurface, ParametricSurface3D, SearchNearestParameter};
|
||
let Some(ctrl) = section_controls(section, round) else { return false };
|
||
let v = fit.range.0 + (fit.range.1 - fit.range.0) * along;
|
||
for u in [0.0, 0.25, 0.5, 0.75, 1.0] {
|
||
let [a, b, c] = ctrl;
|
||
let h = a * (1.0 - u) * (1.0 - u) + b * (2.0 * u * (1.0 - u)) + c * u * u;
|
||
if !h.w.is_finite() || h.w <= 1e-12 {
|
||
return false;
|
||
}
|
||
let exact = Vec3::new(h.x / h.w, h.y / h.w, h.z / h.w);
|
||
// The input intersection edge may use piecewise leader-curve parameters. A
|
||
// parameter-speed change is not a geometric error: compare nearest points, with
|
||
// contacts constrained to the corresponding boundary and all probes to the strip.
|
||
let (probe_u, probe_v) = if u == 0.0 || u == 1.0 {
|
||
let Some(contact) = fit.contacts.get(usize::from(u == 1.0)) else { return false };
|
||
let Some(parameter) = contact.search_nearest_parameter(p3(exact), v, 32) else { return false };
|
||
(u, parameter.clamp(fit.range.0, fit.range.1))
|
||
} else {
|
||
let Some((pu, pv)) = fit.surface.search_nearest_parameter(p3(exact), Some((u, v)), 32) else { return false };
|
||
(pu.clamp(0.0, 1.0), pv.clamp(fit.range.0, fit.range.1))
|
||
};
|
||
let actual = from_p3(fit.surface.subs(probe_u, probe_v));
|
||
if !actual.is_finite() || actual.dist(exact) > tolerance {
|
||
return false;
|
||
}
|
||
if round && (u == 0.0 || u == 1.0) {
|
||
let Some(want) = ((section.c - exact) * sigma).normalized() else { return false };
|
||
let normal = fit.surface.normal(probe_u, probe_v);
|
||
let Some(normal) = Vec3::new(normal.x, normal.y, normal.z).normalized() else { return false };
|
||
if normal.dot(want).abs() < 0.5f64.to_radians().cos() {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
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, 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());
|
||
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)));
|
||
}
|
||
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)));
|
||
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())
|
||
}
|
||
|
||
/// 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 {
|
||
c: Vec3,
|
||
q1: Vec3,
|
||
q2: Vec3,
|
||
}
|
||
|
||
/// The third condition of a section's solve, in place of staying in the plane square to the edge: a contact held at a seam.
|
||
#[derive(Clone, Copy)]
|
||
enum Extra<'a> {
|
||
/// Side `k`'s contact lies in this plane (a point on it, its normal): beside a straight seam.
|
||
Plane(usize, Vec3, Vec3),
|
||
/// Side `k`'s contact lies on this curved seam (a lofted wall's seam between its two halves).
|
||
Seam(usize, &'a mt::Edge),
|
||
}
|
||
|
||
/// The seam's point nearest `x` and its unit tangent there.
|
||
fn nearest_on_seam(seam: &mt::Edge, x: Vec3) -> Option<(Vec3, Vec3)> {
|
||
use mt::{BoundedCurve, ParametricCurve, SearchNearestParameter};
|
||
let curve = seam.curve();
|
||
let (lo, hi) = curve.range_tuple();
|
||
let at = |t: f64| from_p3(curve.subs(t));
|
||
// The nearest of a few samples starts the search: a long seam holds the point's neighbourhood
|
||
// far from its ends.
|
||
let hint = (0..=64).map(|i| lo + (hi - lo) * i as f64 / 64.0).min_by(|a, b| at(*a).dist(x).total_cmp(&at(*b).dist(x)))?;
|
||
let t = curve.search_nearest_parameter(p3(x), hint, 100)?.clamp(lo, hi);
|
||
let d = curve.der(t);
|
||
Some((at(t), Vec3::new(d.x, d.y, d.z).normalized()?))
|
||
}
|
||
|
||
/// Nearest point of a face's surface and the outward normal there.
|
||
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))?;
|
||
let q = from_p3(s.subs(uv.0, uv.1));
|
||
let n = s.normal(uv.0, uv.1);
|
||
Some((q, Vec3::new(n.x, n.y, n.z).normalized()?, uv))
|
||
}
|
||
|
||
/// Whether `uv` lies within the parameters of the face's surface (unbounded directions pass).
|
||
pub(crate) fn in_range(f: &mt::Face, uv: (f64, f64)) -> bool {
|
||
use mt::ParametricSurface;
|
||
use std::ops::Bound;
|
||
let inside = |range: (Bound<f64>, Bound<f64>), x: f64| {
|
||
let slack = |a: f64, b: f64| (b - a).abs() * 1e-6 + 1e-9;
|
||
match range {
|
||
(Bound::Included(a) | Bound::Excluded(a), Bound::Included(b) | Bound::Excluded(b)) => x >= a - slack(a, b) && x <= b + slack(a, b),
|
||
(Bound::Included(a) | Bound::Excluded(a), Bound::Unbounded) => x >= a - 1e-9,
|
||
(Bound::Unbounded, Bound::Included(b) | Bound::Excluded(b)) => x <= b + 1e-9,
|
||
(Bound::Unbounded, Bound::Unbounded) => true,
|
||
}
|
||
};
|
||
let (ru, rv) = f.oriented_surface().parameter_range();
|
||
uv.0.is_finite() && uv.1.is_finite() && inside(ru, uv.0) && inside(rv, uv.1)
|
||
}
|
||
|
||
/// Solve for the ball touching both sides near `p`. `extra` adds a third condition (a plane the
|
||
/// side-k contact must lie in, or a curved seam it must lie on); 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, cusp: f64, extra: Option<Extra<'_>>, tol: f64) -> Option<Section> {
|
||
solve_from(f1, f2, p, t, r, sigma, cusp, extra, tol, None)
|
||
}
|
||
|
||
/// [`solve`], started from the centre `start` when given (the corner builder's balls at a knife
|
||
/// edge start beside the last one).
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn solve_from(
|
||
f1: &[&mt::Face],
|
||
f2: &[&mt::Face],
|
||
p: Vec3,
|
||
t: Vec3,
|
||
r: f64,
|
||
sigma: f64,
|
||
cusp: f64,
|
||
extra: Option<Extra<'_>>,
|
||
tol: f64,
|
||
start: Option<Vec3>,
|
||
) -> 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
|
||
// ran away or settled on the far side), is searched again cold.
|
||
let last = std::cell::RefCell::new(HashMap::<mt::FaceID, ((f64, f64), Vec3)>::new());
|
||
let near = |fs: &[&mt::Face], c: Vec3| -> Option<(Vec3, Vec3)> {
|
||
fs.iter()
|
||
.filter_map(|f| {
|
||
let prev = last.borrow().get(&f.id()).copied();
|
||
let warm =
|
||
prev.and_then(|(uv, q)| project(f, c, Some(uv)).filter(|(x, _, uv)| in_range(f, *uv) && x.dist(c) <= q.dist(c) + tol * 1e-3));
|
||
let (q, n, uv) = match warm {
|
||
Some(x) => x,
|
||
None => project(f, c, None)?,
|
||
};
|
||
last.borrow_mut().insert(f.id(), (uv, q));
|
||
Some((q, n))
|
||
})
|
||
.min_by(|a, b| a.0.dist(c).total_cmp(&b.0.dist(c)))
|
||
};
|
||
let (q1, n1) = near(f1, p)?;
|
||
let (q2, n2) = near(f2, p)?;
|
||
let _ = (q1, q2);
|
||
let den = 1.0 + n1.dot(n2);
|
||
let mut c = if let Some(c) = start {
|
||
c
|
||
} else 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)?;
|
||
let g1 = (c - q1).dot(n1) - sigma * r;
|
||
let g2 = (c - q2).dot(n2) - sigma * r;
|
||
// Third row: stay in the section plane, or put a contact on the seam plane.
|
||
let (row3, g3) = match extra {
|
||
None => (t, (c - p).dot(t)),
|
||
Some(Extra::Plane(k, o, m)) => {
|
||
let q = if k == 1 { q1 } else { q2 };
|
||
(m, (q - o).dot(m))
|
||
}
|
||
Some(Extra::Seam(k, seam)) => {
|
||
// The plane through the seam's nearest point to the contact, square to that side's
|
||
// surface, taken afresh each step: it holds the contact on the curved seam as the
|
||
// steps close in.
|
||
let (q, n) = if k == 1 { (q1, n1) } else { (q2, n2) };
|
||
let (on_seam, d) = nearest_on_seam(seam, q)?;
|
||
let m = d.cross(n).normalized()?;
|
||
(m, (q - on_seam).dot(m))
|
||
}
|
||
};
|
||
let a = [[n1.x, n1.y, n1.z], [n2.x, n2.y, n2.z], [row3.x, row3.y, row3.z]];
|
||
let b = [-g1, -g2, -g3];
|
||
let x = crate::polyhedron::solve3_pub(a, b)?;
|
||
let dc = Vec3::new(x[0], x[1], x[2]);
|
||
c += dc;
|
||
if dc.len() < tol * 1e-3 && g1.abs() < tol && g2.abs() < tol && g3.abs() < tol {
|
||
let (q1, _) = near(f1, c)?;
|
||
let (q2, _) = near(f2, c)?;
|
||
return Some(Section { c, q1, q2 });
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// Whether the strip's curvature jumps where two runs of sections meet (each three sections from
|
||
/// the joint outward, the joint's first): for the centres or either contact, the difference of
|
||
/// the circles through them would move the middle section by more than a tenth of `tolerance`.
|
||
fn bends(a: [&Section; 3], b: [&Section; 3], tolerance: f64) -> bool {
|
||
let curvature = |p: [Vec3; 3]| {
|
||
let (x, y, z) = (p[1] - p[0], p[2] - p[1], p[2] - p[0]);
|
||
let d = x.len() * y.len() * z.len();
|
||
if d > 1e-300 { 2.0 * x.cross(y).len() / d } else { 0.0 }
|
||
};
|
||
let h = a[0].c.dist(a[1].c);
|
||
[|s: &Section| s.c, |s: &Section| s.q1, |s: &Section| s.q2]
|
||
.iter()
|
||
.any(|f| (curvature(a.map(f)) - curvature(b.map(f))).abs() * h * h > tolerance * 0.1)
|
||
}
|
||
|
||
/// Homogeneous control points of the circular (or straight) cross-section from q1 to q2.
|
||
fn section_controls(s: &Section, round: bool) -> Option<[mt::Vector4; 3]> {
|
||
let h = |p: Vec3, w: f64| mt::Vector4::new(p.x * w, p.y * w, p.z * w, w);
|
||
if !round {
|
||
let m = (s.q1 + s.q2) * 0.5;
|
||
return Some([h(s.q1, 1.0), h(m, 1.0), h(s.q2, 1.0)]);
|
||
}
|
||
let (a, b) = (s.q1 - s.c, s.q2 - s.c);
|
||
let theta = a.normalized()?.dot(b.normalized()?).clamp(-1.0, 1.0).acos();
|
||
if !(theta > 1e-6 && theta < std::f64::consts::PI - 1e-6) {
|
||
return None;
|
||
}
|
||
let half = (theta / 2.0).cos();
|
||
let bis = (a + b).normalized()?;
|
||
let m = s.c + bis * (a.len() / half);
|
||
Some([h(s.q1, 1.0), h(m, half), h(s.q2, 1.0)])
|
||
}
|
||
|
||
/// Where to start the ball at a knife edge through `p` (direction `t`, the sides running on from
|
||
/// it along `into`): the first point along them, from the edge, where they are a diameter
|
||
/// apart, midway.
|
||
fn knife_start(o1: &[&mt::Face], o2: &[&mt::Face], p: Vec3, t: Vec3, into: Vec3, r: f64) -> Option<Vec3> {
|
||
let w = (into - t * into.dot(t)).normalized()?;
|
||
(1..=256).find_map(|k| {
|
||
let x = p + w * (r * k as f64 / 8.0);
|
||
let a = o1.iter().filter_map(|f| project(f, x, None)).min_by(|a, b| a.0.dist(x).total_cmp(&b.0.dist(x)))?;
|
||
let b = o2.iter().filter_map(|f| project(f, x, None)).min_by(|a, b| a.0.dist(x).total_cmp(&b.0.dist(x)))?;
|
||
let c = (a.0 + b.0) * 0.5;
|
||
(a.0.dist(b.0) >= 2.0 * r).then(|| c - t * (c - p).dot(t))
|
||
})
|
||
}
|
||
|
||
/// Whether two faces meeting at no angle at `pm` (the first's outward normal `n1`, running on
|
||
/// from the edge along `into`) hold the material between them: a little way in, the second is
|
||
/// behind the first.
|
||
fn knife_convex(f1: &mt::Face, f2: &mt::Face, pm: Vec3, into: Vec3, r: f64) -> Option<bool> {
|
||
let x = pm + into * r;
|
||
let (a, b) = (project(f1, x, None)?, project(f2, x, None)?);
|
||
Some((b.0 - a.0).dot(a.1) < 0.0)
|
||
}
|
||
|
||
/// The nearest point of a face's (oriented) surface to `x`, its outward normal and parameters;
|
||
/// `last`: where the last search on it ended (the points asked about come in small steps).
|
||
fn nearest_on(surface: &mt::Surface, last: &std::cell::Cell<Option<(f64, f64)>>, x: Vec3) -> Option<(Vec3, Vec3, (f64, f64))> {
|
||
use mt::{ParametricSurface, ParametricSurface3D, SearchNearestParameter};
|
||
// The nearest of the search from the last answer, a fresh one (the first time, also from
|
||
// a grid over the surface's parameters: a fresh search on a conic can settle on its far
|
||
// side) and either kept within the parameters (past them a conic runs on round).
|
||
let (ru, rv) = surface.try_range_tuple();
|
||
let ((u0, u1), (v0, v1)) = (ru.unwrap_or((f64::NEG_INFINITY, f64::INFINITY)), rv.unwrap_or((f64::NEG_INFINITY, f64::INFINITY)));
|
||
let inside = |uv: (f64, f64)| uv.0 >= u0 - 1e-9 && uv.0 <= u1 + 1e-9 && uv.1 >= v0 - 1e-9 && uv.1 <= v1 + 1e-9;
|
||
let warm = last.get().and_then(|hint| surface.search_nearest_parameter(p3(x), Some(hint), 20));
|
||
let mut found: Vec<(f64, f64)> = warm.into_iter().collect();
|
||
if !warm.is_some_and(inside) {
|
||
found.extend(surface.search_nearest_parameter(p3(x), None, 100));
|
||
}
|
||
if last.get().is_none() && [u0, u1, v0, v1].iter().all(|x| x.is_finite()) {
|
||
for i in 0..5 {
|
||
for j in 0..5 {
|
||
let hint = (u0 + (u1 - u0) * (i as f64 + 0.5) / 5.0, v0 + (v1 - v0) * (j as f64 + 0.5) / 5.0);
|
||
found.extend(surface.search_nearest_parameter(p3(x), Some(hint), 50));
|
||
}
|
||
}
|
||
}
|
||
let clamped: Vec<(f64, f64)> = found.iter().map(|(u, v)| (u.clamp(u0, u1), v.clamp(v0, v1))).collect();
|
||
found.extend(clamped);
|
||
let d = |uv: &(f64, f64)| from_p3(surface.subs(uv.0, uv.1)).dist(x);
|
||
let uv = found.into_iter().filter(|uv| d(uv).is_finite()).min_by(|a, b| d(a).total_cmp(&d(b)))?;
|
||
last.set(Some(uv));
|
||
let n = surface.normal(uv.0, uv.1);
|
||
Some((from_p3(surface.subs(uv.0, uv.1)), Vec3::new(n.x, n.y, n.z).normalized()?, uv))
|
||
}
|
||
|
||
/// The strip with parameters in proportion to its lengths across and along (the same surface).
|
||
/// The mesher triangulates a face in its parameters: on a unit square for a strip a few
|
||
/// millimetres wide and tens long, the points of a slanted end fan out into triangles so long
|
||
/// along the curve that they turn over.
|
||
fn in_proportion(surface: &mt::Surface) -> Result<mt::Surface> {
|
||
use mt::ParametricSurface;
|
||
let fail = || KernelError::Failed("curve blend: strip parameters".into());
|
||
let mt::Surface::NurbsSurface(ns) = surface else { return Err(fail()) };
|
||
let length = |at: &dyn Fn(f64) -> Vec3| (0..32).map(|i| at(i as f64 / 32.0).dist(at((i + 1) as f64 / 32.0))).sum::<f64>();
|
||
let across = length(&|u| from_p3(surface.subs(u, 0.5)));
|
||
let along = length(&|v| from_p3(surface.subs(0.5, v)));
|
||
if !(across.is_finite() && along.is_finite() && across > 1e-9 && along > 1e-9) {
|
||
return Err(fail());
|
||
}
|
||
let bs = ns.non_rationalized();
|
||
let (mut uk, mut vk) = bs.knot_vecs().clone();
|
||
uk.transform(across, 0.0);
|
||
vk.transform(along, 0.0);
|
||
let bs = mt::BSplineSurface::try_new((uk, vk), bs.control_points().clone()).map_err(|_| fail())?;
|
||
Ok(mt::Surface::NurbsSurface(mt::NurbsSurface::new(bs)))
|
||
}
|
||
|
||
/// A chain edge the ball turns about instead of rolling along: an arc of the blend's radius on a
|
||
/// planar side (`side`), curving round the ball (a rounded corner rounded again). The ball's
|
||
/// centre stays at `c`, it touches the planar side only at the arc's centre `o`, and the blend
|
||
/// there is a piece of sphere (of cone, for a chamfer) about the axis through `c` along `axis`.
|
||
#[derive(Clone, Copy, Debug)]
|
||
struct Pivot {
|
||
c: Vec3,
|
||
o: Vec3,
|
||
axis: Vec3,
|
||
side: usize,
|
||
}
|
||
|
||
/// The circle an edge lies on (centre, radius, axis), if it is an arc of one within `tol`.
|
||
pub(crate) fn circle_of(e: &mt::Edge, tol: f64) -> Option<(Vec3, f64, Vec3)> {
|
||
use mt::{BoundedCurve, ParametricCurve};
|
||
let curve = e.oriented_curve();
|
||
let (t0, t1) = curve.range_tuple();
|
||
let at = |f: f64| from_p3(curve.subs(t0 + (t1 - t0) * f));
|
||
let (a, u, v) = (at(0.0), at(0.5) - at(0.0), at(1.0) - at(0.0));
|
||
let w = u.cross(v);
|
||
if w.len2() < 1e-24 {
|
||
return None;
|
||
}
|
||
let o = a + (v * u.len2() - u * v.len2()).cross(w) * (1.0 / (2.0 * w.len2()));
|
||
let (rho, axis) = (o.dist(a), w.normalized()?);
|
||
(0..=8).map(|i| at(i as f64 / 8.0)).all(|p| (p.dist(o) - rho).abs() <= tol && (p - o).dot(axis).abs() <= tol).then_some((o, rho, axis))
|
||
}
|
||
|
||
/// The surface the section (homogeneous controls from contact 1 to contact 2) sweeps turning by
|
||
/// `angle` about the axis through `c` along the unit `axis`: rational quadratic arcs in v, a
|
||
/// quarter turn at most each, so it is exact.
|
||
fn revolve(ctrl: [mt::Vector4; 3], c: Vec3, axis: Vec3, angle: f64) -> Option<mt::Surface> {
|
||
let pieces = ((angle.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).clamp(1, 8);
|
||
let step = angle / pieces as f64;
|
||
let half = (step / 2.0).cos();
|
||
if !(half.is_finite() && half > 1e-6) {
|
||
return None;
|
||
}
|
||
let rot = |x: Vec3, phi: f64| {
|
||
let (s, co) = phi.sin_cos();
|
||
x * co + axis.cross(x) * s + axis * (axis.dot(x) * (1.0 - co))
|
||
};
|
||
let h = |p: Vec3, w: f64| mt::Vector4::new(p.x * w, p.y * w, p.z * w, w);
|
||
let mut rows = Vec::with_capacity(3);
|
||
for q in ctrl {
|
||
if !(q.w.is_finite() && q.w > 1e-12) {
|
||
return None;
|
||
}
|
||
let x = Vec3::new(q.x / q.w, q.y / q.w, q.z / q.w) - c;
|
||
let along = axis * axis.dot(x);
|
||
let mut row = vec![h(c + x, q.w)];
|
||
for i in 0..pieces {
|
||
let a0 = step * i as f64;
|
||
row.push(h(c + along + rot(x - along, a0 + step / 2.0) * (1.0 / half), q.w * half));
|
||
row.push(h(c + rot(x, a0 + step), q.w));
|
||
}
|
||
rows.push(row);
|
||
}
|
||
let mut knots = vec![0.0; 3];
|
||
for i in 1..pieces {
|
||
let k = i as f64 / pieces as f64;
|
||
knots.extend([k, k]);
|
||
}
|
||
knots.extend([1.0; 3]);
|
||
let bs = mt::BSplineSurface::try_new((mt::KnotVec::bezier_knot(2), mt::KnotVec::from(knots)), rows).ok()?;
|
||
Some(mt::Surface::NurbsSurface(mt::NurbsSurface::new(bs)))
|
||
}
|
||
|
||
/// Whether the edge is straight: a line, or a curve on the segment between its ends within
|
||
/// `tol` (an earlier blend's contact along a straight edge is a fitted curve).
|
||
fn straight(e: &mt::Edge, tol: f64) -> bool {
|
||
use mt::{BoundedCurve, ParametricCurve};
|
||
let c = e.curve();
|
||
if matches!(c, mt::Curve::Line(_)) {
|
||
return true;
|
||
}
|
||
let (a, b) = (from_p3(e.front().point()), from_p3(e.back().point()));
|
||
let (t0, t1) = c.range_tuple();
|
||
a.dist(b) > tol && (0..=16).all(|i| from_p3(c.subs(t0 + (t1 - t0) * i as f64 / 16.0)).dist_to_segment(a, b) <= tol)
|
||
}
|
||
|
||
/// Consuming both seams of a side can leave its old boundary and a fitted contact on the
|
||
/// same curve (#1113). They are distinct topological edges but enclose no usable face. Keep
|
||
/// genuine two-edge faces (a lens or lune); refuse only coincident traces, in both directions
|
||
/// since the curves need not have the same parameterisation.
|
||
fn collapsed_boundary(edges: &[mt::Edge], tolerance: f64) -> bool {
|
||
use mt::{BoundedCurve, ParametricCurve};
|
||
let [a, b] = edges else { return false };
|
||
[(a, b), (b, a)].into_iter().all(|(edge, other)| {
|
||
let curve = edge.curve();
|
||
let (lo, hi) = curve.range_tuple();
|
||
(0..=16).all(|k| {
|
||
let p = from_p3(curve.subs(lo + (hi - lo) * f64::from(k) / 16.0));
|
||
crate::blend::on_edge(other, p, tolerance).is_some()
|
||
})
|
||
})
|
||
}
|
||
|
||
/// A curved seam (an arc of an end face) shortened to the blend's contacts: the part of its
|
||
/// curve between the new ends `a` and `b` (`None` keeps that end), which must lie on it, in
|
||
/// order, short of each other.
|
||
pub(crate) fn trim_curved_seam(seam: &mt::Edge, a: Option<mt::Vertex>, b: Option<mt::Vertex>, size: f64) -> Result<mt::Edge> {
|
||
// A closed seam (a full circle starting and ending at one vertex) has no "between": which
|
||
// end a contact replaces, and which way round the kept piece runs, are both ambiguous.
|
||
if seam.front() == seam.back() {
|
||
return Err(unsupported("a closed curved edge where the blend ends"));
|
||
}
|
||
use mt::{BoundedCurve, Cut, SearchNearestParameter};
|
||
let mut curve = seam.curve();
|
||
let (lo, hi) = curve.range_tuple();
|
||
let at = |v: &Option<mt::Vertex>, end: f64| -> Result<f64> {
|
||
let Some(v) = v else { return Ok(end) };
|
||
// Start from the nearest of a few samples: from the far end of a long arc
|
||
// the search can settle on the opposite side of the circle.
|
||
let q = from_p3(v.point());
|
||
let hint = (0..=32)
|
||
.map(|i| lo + (hi - lo) * i as f64 / 32.0)
|
||
.min_by(|x, y| {
|
||
let d = |t: f64| from_p3(mt::ParametricCurve::subs(&curve, t)).dist(q);
|
||
d(*x).total_cmp(&d(*y))
|
||
})
|
||
.unwrap_or(end);
|
||
let t = curve.search_nearest_parameter(v.point(), hint, 100).ok_or_else(|| unsupported("the blend is larger than the neighbouring faces"))?;
|
||
if !(t.is_finite() && from_p3(mt::ParametricCurve::subs(&curve, t)).dist(from_p3(v.point())) <= size * 1e-5) {
|
||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||
}
|
||
Ok(t)
|
||
};
|
||
let (ta, tb) = (at(&a, lo)?, at(&b, hi)?);
|
||
let margin = (hi - lo) * 1e-9;
|
||
if ta < lo - margin || tb > hi + margin || tb - ta < (hi - lo) * 1e-6 {
|
||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||
}
|
||
if tb < hi {
|
||
let _ = curve.cut(tb);
|
||
}
|
||
let piece = if ta > lo { curve.cut(ta) } else { curve };
|
||
let a = a.unwrap_or_else(|| seam.front().clone());
|
||
let b = b.unwrap_or_else(|| seam.back().clone());
|
||
mt::Edge::try_new(&a, &b, piece).map_err(|e| KernelError::Failed(format!("curve blend: seam: {e}")))
|
||
}
|
||
|
||
/// Blend the closed chain of edges (ids in any order) with radius `r`; with what it built from
|
||
/// `solid` (each blend face in place of its chain edge).
|
||
pub(crate) fn curve_blend(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64, round: bool) -> Result<(Solid, Rebuild)> {
|
||
curve_blend_with_limit(solid, ids, size, r, round, MAX_SAMPLES)
|
||
}
|
||
|
||
fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64, round: bool, max_samples: usize) -> Result<(Solid, Rebuild)> {
|
||
if !(size.is_finite() && size > 0.0 && r.is_finite() && r > 0.0) {
|
||
return Err(KernelError::Invalid("curve blend size and radius must be finite and positive".into()));
|
||
}
|
||
if !(4..=MAX_SAMPLES).contains(&max_samples) {
|
||
return Err(KernelError::Invalid("curve blend section limit is outside the supported range".into()));
|
||
}
|
||
let tol = (size * 1e-7).max(1e-9);
|
||
let shells = solid.boundaries();
|
||
let (si, shell) = shells
|
||
.iter()
|
||
.enumerate()
|
||
.find(|(_, sh)| sh.edge_iter().any(|e| ids.contains(&e.id())))
|
||
.ok_or_else(|| KernelError::Failed("edge not found".into()))?;
|
||
let faces: Vec<mt::Face> = shell.face_iter().cloned().collect();
|
||
// Order the chain: edges as stored (absolute direction), each starting where the last ended.
|
||
let mut chain: Vec<mt::Edge> = Vec::new();
|
||
let all: Vec<mt::Edge> = {
|
||
let mut seen = std::collections::HashSet::new();
|
||
shell.edge_iter().filter(|e| ids.contains(&e.id()) && seen.insert(e.id())).map(|e| e.absolute_clone()).collect()
|
||
};
|
||
// An open chain starts at a vertex only one of its edges touches.
|
||
let uses = |v: &mt::Vertex| all.iter().filter(|e| e.front() == v || e.back() == v).count();
|
||
let first = match all.iter().find_map(|e| {
|
||
if uses(e.front()) == 1 {
|
||
Some(e.clone())
|
||
} else if uses(e.back()) == 1 {
|
||
Some(e.inverse())
|
||
} else {
|
||
None
|
||
}
|
||
}) {
|
||
Some(e) => e,
|
||
None => all.first().cloned().ok_or_else(|| KernelError::Failed("no edges".into()))?,
|
||
};
|
||
chain.push(first.clone());
|
||
while chain.len() < all.len() {
|
||
let end = chain.last().map(|e| e.back().clone()).ok_or_else(|| KernelError::Failed("chain".into()))?;
|
||
let next = all
|
||
.iter()
|
||
.find(|e| !chain.iter().any(|c| c.id() == e.id()) && (*e.front() == end || *e.back() == end))
|
||
.ok_or_else(|| unsupported("the edges don't form one chain"))?;
|
||
chain.push(if *next.front() == end { next.clone() } else { next.inverse() });
|
||
}
|
||
let closed = chain.first().map(|e| e.front().clone()) == chain.last().map(|e| e.back().clone());
|
||
let n = chain.len();
|
||
// The chain's vertices (n for a closed chain, n + 1 for an open one).
|
||
let mut verts: Vec<mt::Vertex> = chain.iter().map(|e| e.front().clone()).collect();
|
||
if !closed && let Some(e) = chain.last() {
|
||
verts.push(e.back().clone());
|
||
}
|
||
let nv = verts.len();
|
||
// Faces of each edge, split into the two sides.
|
||
let faces_of = |e: &mt::Edge| -> Vec<usize> {
|
||
(0..faces.len()).filter(|i| faces.get(*i).is_some_and(|f| f.edge_iter().any(|x| x.id() == e.id()))).collect()
|
||
};
|
||
let mut side1: Vec<usize> = Vec::new();
|
||
let mut side2: Vec<usize> = Vec::new();
|
||
for (k, e) in chain.iter().enumerate() {
|
||
let fs = faces_of(e);
|
||
let [a, b] = fs[..] else { return Err(unsupported("an edge without two faces")) };
|
||
if k == 0 {
|
||
side1.push(a);
|
||
side2.push(b);
|
||
continue;
|
||
}
|
||
// The side-1 face of this edge is the one whose normal matches side 1 at the joint.
|
||
let p = from_p3(e.front().point());
|
||
let nrm = |fi: usize| faces.get(fi).and_then(|f| project(f, p, None)).map(|x| x.1);
|
||
let prev1 = side1.last().copied().and_then(nrm);
|
||
let (na, nb) = (nrm(a), nrm(b));
|
||
let a_is_1 = match (prev1, na, nb) {
|
||
(Some(x), Some(y), Some(z)) => x.dot(y) >= x.dot(z),
|
||
_ => return Err(KernelError::Failed("blend: normals".into())),
|
||
};
|
||
if a_is_1 {
|
||
side1.push(a);
|
||
side2.push(b);
|
||
} else {
|
||
side1.push(b);
|
||
side2.push(a);
|
||
}
|
||
}
|
||
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()))?;
|
||
let in_f1 = f1.boundary_iters().into_iter().flatten().find(|e| e.id() == e0.id()).ok_or_else(|| KernelError::Failed("edge use".into()))?;
|
||
let (pm, t1) = {
|
||
use mt::{BoundedCurve, ParametricCurve};
|
||
let c = in_f1.oriented_curve();
|
||
let (a, b) = c.range_tuple();
|
||
let m = (a + b) * 0.5;
|
||
let d = c.der(m);
|
||
(from_p3(c.subs(m)), Vec3::new(d.x, d.y, d.z).normalized().ok_or_else(|| KernelError::Failed("tangent".into()))?)
|
||
};
|
||
let (n1, n2) = match (project(f1, pm, None), s2.first().and_then(|f| project(f, pm, None))) {
|
||
(Some(x), Some(y)) => (x.1, y.1),
|
||
_ => return Err(KernelError::Failed("blend: normals".into())),
|
||
};
|
||
// 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"));
|
||
}
|
||
// Arcs on a planar side that curve round the ball: its centre runs round them at their
|
||
// radius less the blend's. Less than nothing is too large; nothing is a pivot.
|
||
let mut pivots: Vec<Option<Pivot>> = vec![None; n];
|
||
for (k, e) in chain.iter().enumerate() {
|
||
let Some((o, rho, _)) = circle_of(e, tol * 1e3) else { continue };
|
||
for side in [1, 2] {
|
||
let Some(f) = (if side == 1 { side1.get(k) } else { side2.get(k) }).and_then(|i| faces.get(*i)) else { continue };
|
||
let mt::Surface::Plane(pl) = f.oriented_surface() else { continue };
|
||
let Some(np) = Vec3::new(pl.normal().x, pl.normal().y, pl.normal().z).normalized() else { continue };
|
||
// Into the face (left of the edge as the face runs it, about its outward normal).
|
||
let Some(used) = f.boundary_iters().into_iter().flatten().find(|x| x.id() == e.id()) else { continue };
|
||
let Some((pm, tm)) = ({
|
||
use mt::{BoundedCurve, ParametricCurve};
|
||
let c = used.oriented_curve();
|
||
let (a, b) = c.range_tuple();
|
||
let d = c.der((a + b) * 0.5);
|
||
Vec3::new(d.x, d.y, d.z).normalized().map(|t| (from_p3(c.subs((a + b) * 0.5)), t))
|
||
}) else {
|
||
continue;
|
||
};
|
||
if (o - pm).dot(np.cross(tm)) <= 0.0 {
|
||
continue;
|
||
}
|
||
if rho < r - tol * 1e3 {
|
||
return Err(too_large("the blend is larger than a corner radius"));
|
||
}
|
||
if (rho - r).abs() > tol * 1e3 {
|
||
continue;
|
||
}
|
||
// The ball sits on the planar side at the arc's centre; the other side must be a round
|
||
// about the same axis, the ball touching it all along.
|
||
let c = o + np * (sigma * r);
|
||
let (a1, a2) = edge_sides(&[k]);
|
||
let other = if side == 1 { a2 } else { a1 };
|
||
let reach = other.iter().filter_map(|f| project(f, c, None)).map(|x| x.0.dist(c)).fold(f64::INFINITY, f64::min);
|
||
if (reach - r).abs() <= size * 1e-4 {
|
||
if let Some(slot) = pivots.get_mut(k) {
|
||
*slot = Some(Pivot { c, o, axis: np, side });
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if pivots.iter().all(Option::is_some) {
|
||
return Err(unsupported("rounding again only rounded corners of the blend's radius"));
|
||
}
|
||
// Seams at the chain's vertices: edges there that aren't in the chain, and their side.
|
||
let chain_ids: Vec<mt::EdgeID> = chain.iter().map(|e| e.id()).collect();
|
||
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)
|
||
// A flat strip on planar sides can end across two planar faces (an earlier chamfer).
|
||
// Their shared seam is shortened to the crossing of the new chamfer plane.
|
||
let mut split_ends: Vec<(usize, usize, usize, mt::Edge)> = 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) = {
|
||
let ei = if k == nv - 1 && !closed { n - 1 } else { k.min(n - 1) };
|
||
(side1.get(ei).copied().unwrap_or(usize::MAX), side2.get(ei).copied().unwrap_or(usize::MAX))
|
||
};
|
||
let mut found: Vec<(usize, mt::Edge)> = Vec::new();
|
||
for (fi, f) in faces.iter().enumerate() {
|
||
if !f.vertex_iter().any(|x| x == *v) {
|
||
continue;
|
||
}
|
||
let on_side = side1.contains(&fi) || side2.contains(&fi);
|
||
if !on_side {
|
||
if !at_end || end_faces.iter().filter(|(kk, _)| *kk == k).count() >= 2 {
|
||
return Err(unsupported("a vertex of the edge chain where other faces meet"));
|
||
}
|
||
end_faces.push((k, fi));
|
||
continue;
|
||
}
|
||
for x in f.edge_iter() {
|
||
if chain_ids.contains(&x.id()) || !(*x.front() == *v || *x.back() == *v) || found.iter().any(|(_, y)| y.id() == x.id()) {
|
||
continue;
|
||
}
|
||
let side = if fi == k1 || (side1.contains(&fi) && fi != k2) { 1 } else { 2 };
|
||
found.push((side, x.absolute_clone()));
|
||
}
|
||
}
|
||
if at_end && !end_faces.iter().any(|(kk, _)| *kk == k) {
|
||
return Err(unsupported("an open chain of curved edges must end at a face"));
|
||
}
|
||
let ends: Vec<usize> = end_faces.iter().filter(|(kk, _)| *kk == k).map(|(_, fi)| *fi).collect();
|
||
if let [ga, gb] = ends[..] {
|
||
let terminal = chain.get(if k == 0 { 0 } else { n - 1 }).ok_or_else(|| unsupported("chain end"))?;
|
||
let planar = [ga, gb, k1, k2].iter().all(|i| faces.get(*i).and_then(crate::blend::plane_normal).is_some());
|
||
if round || !planar || !straight(terminal, tol * 10.0) {
|
||
return Err(unsupported("a vertex of the edge chain where other faces meet"));
|
||
}
|
||
let shares = |g: usize, side: usize| {
|
||
faces
|
||
.get(g)
|
||
.zip(faces.get(side))
|
||
.is_some_and(|(g, s)| g.edge_iter().any(|e| (e.front() == v || e.back() == v) && s.edge_iter().any(|x| x.id() == e.id())))
|
||
};
|
||
let (ga, gb) = if shares(ga, k1) && shares(gb, k2) {
|
||
(ga, gb)
|
||
} else if shares(gb, k1) && shares(ga, k2) {
|
||
(gb, ga)
|
||
} else {
|
||
return Err(unsupported("the end faces do not meet both sides of the chain"));
|
||
};
|
||
let seam = faces
|
||
.get(ga)
|
||
.and_then(|a| {
|
||
faces.get(gb).and_then(|b| a.edge_iter().find(|e| (e.front() == v || e.back() == v) && b.edge_iter().any(|x| x.id() == e.id())))
|
||
})
|
||
.ok_or_else(|| unsupported("the end faces have no shared seam"))?;
|
||
if !straight(&seam, tol * 10.0) {
|
||
return Err(unsupported("a curved seam between the end faces"));
|
||
}
|
||
// The first face is the one reached by side 1's contact.
|
||
end_faces.retain(|(kk, _)| *kk != k);
|
||
end_faces.extend([(k, ga), (k, gb)]);
|
||
split_ends.push((k, ga, gb, seam.absolute_clone()));
|
||
}
|
||
// At most one seam per side.
|
||
if found.iter().filter(|x| x.0 == 1).count() > 1 || found.iter().filter(|x| x.0 == 2).count() > 1 {
|
||
return Err(unsupported("a vertex of the edge chain where several seams meet"));
|
||
}
|
||
seams.push(found);
|
||
}
|
||
// Sections: at each vertex (a contact on the seam there), then along each edge.
|
||
let tangent_at = |e: &mt::Edge, s: f64| -> Option<(Vec3, Vec3)> {
|
||
use mt::{BoundedCurve, ParametricCurve};
|
||
let c = e.oriented_curve();
|
||
let (a, b) = c.range_tuple();
|
||
let t = a + (b - a) * s;
|
||
let d = c.der(t);
|
||
Some((from_p3(c.subs(t)), Vec3::new(d.x, d.y, d.z).normalized()?))
|
||
};
|
||
let mut vsec: Vec<Section> = Vec::with_capacity(nv);
|
||
for k in 0..nv {
|
||
let (p, t) = if k < n {
|
||
tangent_at(chain.get(k).ok_or_else(|| KernelError::Failed("edge".into()))?, 0.0)
|
||
} else {
|
||
tangent_at(chain.get(n - 1).ok_or_else(|| KernelError::Failed("edge".into()))?, 1.0)
|
||
}
|
||
.ok_or_else(|| KernelError::Failed("tangent".into()))?;
|
||
let here = seams.get(k).cloned().unwrap_or_default();
|
||
let at_end = end_faces.iter().any(|(kk, _)| *kk == k);
|
||
// Next to a pivot the ball is at its centre, touching the other side square above the
|
||
// vertex.
|
||
let pivoting: Vec<Pivot> = [(k < n).then_some(k), if k > 0 { Some(k - 1) } else { closed.then_some(n - 1) }]
|
||
.into_iter()
|
||
.flatten()
|
||
.filter_map(|i| pivots.get(i).copied().flatten())
|
||
.collect();
|
||
if let Some(pv) = pivoting.first().copied() {
|
||
if at_end || pivoting.iter().any(|x| x.c.dist(pv.c) > tol * 1e3) {
|
||
return Err(unsupported("a rounded corner of the blend's radius where the edge chain ends or turns"));
|
||
}
|
||
let q = pv.c + (p - pv.o);
|
||
vsec.push(if pv.side == 1 { Section { c: pv.c, q1: pv.o, q2: q } } else { Section { c: pv.c, q1: q, q2: pv.o } });
|
||
continue;
|
||
}
|
||
if at_end {
|
||
// 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 square = match g.oriented_surface() {
|
||
mt::Surface::Plane(pl) => {
|
||
let m = pl.normal();
|
||
Vec3::new(m.x, m.y, m.z).normalized().is_some_and(|ng| ng.dot(t).abs() >= 1.0 - 1e-6)
|
||
}
|
||
_ => false,
|
||
};
|
||
if !square || split_ends.iter().any(|e| e.0 == k) {
|
||
slanted.push((k, fi, p, t, None));
|
||
}
|
||
}
|
||
let extra = match here.first().filter(|_| !at_end) {
|
||
None => None,
|
||
// A curved seam holds the contact on itself (the chain runs on smoothly across it: a
|
||
// lofted wall's two halves).
|
||
Some((side, seam)) if !straight(seam, tol * 10.0) => Some(Extra::Seam(*side, seam)),
|
||
Some((side, seam)) => {
|
||
let side = *side;
|
||
// The plane through the seam square to that side's surface.
|
||
let o = from_p3(seam.front().point());
|
||
let d = (from_p3(seam.back().point()) - o).normalized().ok_or_else(|| KernelError::Failed("seam".into()))?;
|
||
let fs = if side == 1 { &s1 } else { &s2 };
|
||
// 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(Extra::Plane(side, o, d.cross(nn).normalized().ok_or_else(|| KernelError::Failed("seam plane".into()))?))
|
||
}
|
||
};
|
||
// On the faces of the edge from this vertex alone (the last edge's at an open chain's
|
||
// end): the other edge's faces are untrimmed here and can run on near the ball where it no
|
||
// longer is (a wall past the round at a corner, which a ball rolling round it pokes
|
||
// through).
|
||
let (a1, a2) = edge_sides(&[k.min(n - 1)]);
|
||
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 };
|
||
let (a, b) = (from_p3(seam.front().point()), from_p3(seam.back().point()));
|
||
if !straight(seam, tol * 10.0) || q.dist_to_segment(a, b) > tol * 1e3 {
|
||
return Err(unsupported("seams on both sides of a corner of the edge chain"));
|
||
}
|
||
}
|
||
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, 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 = split_ends
|
||
.iter()
|
||
.find(|e| e.0 == *k)
|
||
.and_then(|(_, _, gb, _)| faces.get(*gb))
|
||
.and_then(crate::blend::plane_normal)
|
||
.map_or(ng.dot(*t).abs(), |nb| ng.dot(*t).abs().min(nb.dot(*t).abs()));
|
||
// 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 / if tangent.is_some() { 1.0 } else { cos };
|
||
let out = if *k == 0 { -*t } else { *t };
|
||
// 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);
|
||
let mut reach = reach;
|
||
// Past the end of a curved edge the planes square to its end tangent cut the ring of
|
||
// centres further and further round it, until a step jumps across the ring (#1255: a
|
||
// knuckle's cap circle with a round as wide as 0.7 of its radius). The run is shortened
|
||
// then, as long as one of its sections still lies wholly past the end face.
|
||
let ng_out = if ng.dot(out) < 0.0 { -ng } else { ng };
|
||
let past = |sec: &Section| [sec.c, sec.q1, sec.q2].iter().all(|x| (*x - *p).dot(ng_out) > tol);
|
||
let mut fallback: Option<Vec<Section>> = None;
|
||
for attempt in 0..TANGENT_RETRIES {
|
||
list.clear();
|
||
let step = reach / EXTENSION as f64;
|
||
let sections = (1..=EXTENSION).map(|j| {
|
||
let anchor = *p + out * (step * j as f64);
|
||
solve(&e1, &e2, anchor, *t, r, sigma, cusp, None, tol * 100.0)
|
||
});
|
||
match sections.collect::<Option<Vec<_>>>() {
|
||
Some(all) => {
|
||
let smooth = tangent.is_some()
|
||
|| std::iter::once(at.c).chain(all.iter().map(|s| s.c)).collect::<Vec<_>>().windows(2).all(|w| match w {
|
||
[a, b] => a.dist(*b) <= MAX_STEP_STRETCH * step,
|
||
_ => true,
|
||
});
|
||
if smooth {
|
||
// A shortened run must still cross the end face; else the first,
|
||
// full-length run stands.
|
||
if fallback.is_none() || all.iter().any(past) {
|
||
list = all;
|
||
}
|
||
break;
|
||
}
|
||
fallback.get_or_insert(all);
|
||
}
|
||
None if tangent.is_none() || fallback.is_some() || attempt + 1 == TANGENT_RETRIES => break,
|
||
None => {}
|
||
}
|
||
reach *= if tangent.is_some() { 0.5 } else { 0.75 };
|
||
}
|
||
if list.is_empty() {
|
||
list = fallback.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);
|
||
// At a seam the contact lies on the seam plane, which need not be square to the
|
||
// original edge. Its ball centre therefore need not lie in the edge's normal plane.
|
||
// Interpolate that plane's displacement so the interior sections tend to the exact
|
||
// seam sections, rather than leaving a finite jump as sampling gets finer.
|
||
let offsets: Vec<(f64, f64)> = chain
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(k, edge)| {
|
||
let (p0, t0) = tangent_at(edge, 0.0).ok_or_else(|| KernelError::Failed("tangent".into()))?;
|
||
let (p1, t1) = tangent_at(edge, 1.0).ok_or_else(|| KernelError::Failed("tangent".into()))?;
|
||
let start = vsec.get(k).ok_or_else(|| KernelError::Failed("section".into()))?;
|
||
let end = vsec.get((k + 1) % nv).ok_or_else(|| KernelError::Failed("section".into()))?;
|
||
Ok(((start.c - p0).dot(t0), (end.c - p1).dot(t1)))
|
||
})
|
||
.collect::<Result<_>>()?;
|
||
let section_at = |k: usize, edge: &mt::Edge, along: f64| -> Result<Section> {
|
||
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);
|
||
let (a1, a2) = edge_sides(&[k]);
|
||
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 {
|
||
if n.checked_mul(samples).is_none_or(|count| count > MAX_CHAIN_SAMPLES) {
|
||
return Err(unsupported("curve blend exceeds the section fitting limit"));
|
||
}
|
||
// No guide along a pivot: its sections are the two at its ends.
|
||
let mut guides: Vec<Option<Guide>> = Vec::with_capacity(n);
|
||
for (k, e) in chain.iter().enumerate() {
|
||
if pivots.get(k).copied().flatten().is_some() {
|
||
guides.push(None);
|
||
continue;
|
||
}
|
||
let mut list = vec![*vsec.get(k).ok_or_else(|| KernelError::Failed("section".into()))?];
|
||
for j in 1..samples - 1 {
|
||
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(Some(Guide::new(list)?));
|
||
}
|
||
// Intersection curves inherit their leader's piecewise parameter speed. Fitting
|
||
// those parameters creates oscillations even on a geometrically smooth spine.
|
||
// Resample by distance along the rolling centres, projecting each interpolated
|
||
// centre back onto the exact two offset surfaces before it becomes a section.
|
||
let uniform_section = |k: usize, along: f64| -> Result<Section> {
|
||
let guide = guides.get(k).and_then(Option::as_ref).ok_or_else(|| KernelError::Failed("spine guide".into()))?;
|
||
if along == 0.0 {
|
||
return guide.sections.first().copied().ok_or_else(|| KernelError::Failed("section".into()));
|
||
}
|
||
if along == 1.0 {
|
||
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"))?;
|
||
let (a1, a2) = edge_sides(&[k]);
|
||
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| match guides.get(k) {
|
||
Some(Some(_)) => (0..samples).map(|j| uniform_section(k, j as f64 / (samples - 1) as f64)).collect::<Result<Vec<_>>>(),
|
||
_ => Ok(vec![
|
||
*vsec.get(k).ok_or_else(|| KernelError::Failed("section".into()))?,
|
||
*vsec.get((k + 1) % nv).ok_or_else(|| KernelError::Failed("section".into()))?,
|
||
]),
|
||
})
|
||
.collect::<Result<_>>()?;
|
||
// 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(Option::as_ref).and_then(|g| g.lengths.last().copied()).unwrap_or(0.0);
|
||
let rolls = |k: usize| guides.get(k).is_some_and(Option::is_some);
|
||
let mut fits: Vec<Option<Fit>> = Vec::with_capacity(n);
|
||
for k in 0..n {
|
||
// A pivot's surface is made whole (`revolve`); a strip next to one ends on the joint's
|
||
// section on its own.
|
||
if !rolls(k) {
|
||
fits.push(None);
|
||
continue;
|
||
}
|
||
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 (prev, next) = ((k + n - 1) % n, (k + 1) % n);
|
||
let list = esec.get(k).ok_or_else(|| KernelError::Failed("section".into()))?;
|
||
// Not across a joint where the strip's curvature jumps (a line or a wide arc into a
|
||
// small round): a spline smooth through it spreads the jump over the sections beside
|
||
// it, and the error falls only with the square of their spacing. The strip then ends
|
||
// on the joint's section on its own, which the next one shares.
|
||
let smooth = |near: [Option<&Section>; 3], beyond: [Option<&Section>; 3]| match (near, beyond) {
|
||
([Some(a), Some(b), Some(c)], [Some(x), Some(y), Some(z)]) => !bends([a, b, c], [x, y, z], fit_tolerance),
|
||
_ => false,
|
||
};
|
||
let before = match (closed || k > 0) && rolls(prev) {
|
||
true => spaced(prev, true)?
|
||
.map(|b| if smooth([list.first(), list.get(1), list.get(2)], [list.first(), b.get(1), b.first()]) { b } else { Vec::new() }),
|
||
false => Some(Vec::new()),
|
||
};
|
||
let after = match (closed || k + 1 < n) && rolls(next) {
|
||
true => spaced(next, false)?.map(|a| {
|
||
let m = list.len();
|
||
let tail = [list.last(), m.checked_sub(2).and_then(|i| list.get(i)), m.checked_sub(3).and_then(|i| list.get(i))];
|
||
if smooth(tail, [list.last(), a.first(), a.get(1)]) { a } else { Vec::new() }
|
||
}),
|
||
false => Some(Vec::new()),
|
||
};
|
||
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(Some(fit));
|
||
}
|
||
let mut accurate = true;
|
||
'edges: for k in 0..n {
|
||
let Some(Some(fit)) = fits.get(k) else { continue };
|
||
for j in 0..samples - 1 {
|
||
for fraction in [0.25, 0.5, 0.75] {
|
||
let along = (j as f64 + fraction) / (samples - 1) as f64;
|
||
let section = uniform_section(k, along)?;
|
||
if !crate::guard("check curve blend fit", || Ok(section_fits(fit, §ion, along, round, sigma, fit_tolerance)))? {
|
||
accurate = false;
|
||
break 'edges;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if accurate {
|
||
break (esec, fits);
|
||
}
|
||
if samples >= max_samples {
|
||
return Err(KernelError::Failed("curve blend: could not fit the rolling-ball surface within the accuracy and section limits".into()));
|
||
}
|
||
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();
|
||
let mut split_caps: Vec<(usize, mt::Edge, Vec3)> = Vec::new();
|
||
for (k, fi, _, _, tangent) in &slanted {
|
||
let ke = if *k == 0 { 0 } else { n - 1 };
|
||
let fit = fits.get(ke).and_then(Option::as_ref).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 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)];
|
||
if let Some((_, ga, gb, middle)) = split_ends.iter().find(|e| e.0 == *k) {
|
||
use mt::{BSplineCurve, KnotVec, ParametricCurve, SearchNearestParameter};
|
||
let fail = || unsupported("the chamfer does not cross both end faces");
|
||
let p = from_p3(verts.get(*k).ok_or_else(fail)?.point());
|
||
let (_, t) = tangent_at(chain.get(ke).ok_or_else(fail)?, if *k == 0 { 0.0 } else { 1.0 }).ok_or_else(fail)?;
|
||
let hit = |q: Vec3, fi: usize| -> Option<Vec3> {
|
||
let normal = faces.get(fi).and_then(crate::blend::plane_normal)?;
|
||
let den = normal.dot(t);
|
||
(den.abs() > 1e-6).then(|| q - t * (normal.dot(q - p) / den))
|
||
};
|
||
let a = hit(at.q1, *ga).ok_or_else(fail)?;
|
||
let b = hit(at.q2, *gb).ok_or_else(fail)?;
|
||
let m = (at.q2 - at.q1).cross(t).normalized().ok_or_else(fail)?;
|
||
let far = if middle.front() == verts.get(*k).ok_or_else(fail)? { middle.back() } else { middle.front() };
|
||
let u = from_p3(far.point()) - p;
|
||
let den = m.dot(u);
|
||
if den.abs() < tol {
|
||
return Err(fail());
|
||
}
|
||
let fraction = m.dot(at.q1 - p) / den;
|
||
let c = p + u * fraction;
|
||
let across = (c - at.q1).dot(at.q2 - at.q1) / (at.q2 - at.q1).len2();
|
||
if !(fraction > 1e-6 && fraction < 1.0 - 1e-6 && across > 1e-6 && across < 1.0 - 1e-6) {
|
||
return Err(fail());
|
||
}
|
||
let parameter = |row: usize, q: Vec3| -> Option<f64> {
|
||
let curve = fit.contacts.get(row)?;
|
||
let v = curve.search_nearest_parameter(p3(q), None, 100)?;
|
||
(v > fit.range.0 + 1e-6 && v < fit.range.1 - 1e-6 && from_p3(curve.subs(v)).dist(q) <= fit_tolerance).then_some(v)
|
||
};
|
||
let (ta, tb) = (parameter(0, a).ok_or_else(fail)?, parameter(1, b).ok_or_else(fail)?);
|
||
let curve = BSplineCurve::try_new(KnotVec::from(vec![0.0, 0.0, 0.5, 1.0, 1.0]), vec![p3(a), p3(c), p3(b)])
|
||
.map_err(|e| KernelError::Failed(format!("chamfer end: {e}")))?;
|
||
caps.push((*k, ke, ta, tb, mt::Curve::BSplineCurve(curve)));
|
||
split_caps.push((*k, middle.clone(), c));
|
||
continue;
|
||
}
|
||
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.
|
||
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 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));
|
||
}
|
||
}
|
||
// Along a pivot the planar side's contact is one point: one vertex at both its ends.
|
||
for (k, pv) in pivots.iter().enumerate() {
|
||
let Some(pv) = pv else { continue };
|
||
let vs = if pv.side == 1 { &mut v1 } else { &mut v2 };
|
||
if let Some(v) = vs.get(k).cloned()
|
||
&& let Some(slot) = vs.get_mut((k + 1) % nv)
|
||
{
|
||
*slot = v;
|
||
}
|
||
}
|
||
// 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);
|
||
let mut split_arcs: HashMap<usize, Vec<mt::Edge>> = HashMap::new();
|
||
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"))?);
|
||
let e = if round {
|
||
let m = s.c + ((s.q1 - s.c) + (s.q2 - s.c)).normalized().ok_or_else(|| fail("arc"))? * r;
|
||
builder::circle_arc(a, b, p3(m))
|
||
} 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,
|
||
};
|
||
if let Some((_, _, c)) = split_caps.iter().find(|cap| cap.0 == k) {
|
||
let v = builder::vertex(p3(*c));
|
||
split_arcs.insert(k, vec![builder::line(a, &v), builder::line(&v, b)]);
|
||
}
|
||
arcs.push(e);
|
||
}
|
||
// Outward: toward the ball centre on a concave edge, away on a convex one.
|
||
let oriented = |mut surface: mt::Surface, list: &[Section]| -> Result<mt::Surface> {
|
||
use mt::{ParametricSurface3D, SearchNearestParameter};
|
||
let s = list.get(list.len() / 2).ok_or_else(|| fail("section"))?;
|
||
let mid = s.c + ((s.q1 - s.c) + (s.q2 - s.c)).normalized().ok_or_else(|| fail("mid"))? * r;
|
||
if let Some((u, v)) = surface.search_nearest_parameter(p3(mid), None, 100) {
|
||
let nn = surface.normal(u, v);
|
||
let want = (s.c - mid) * sigma;
|
||
if Vec3::new(nn.x, nn.y, nn.z).dot(want) < 0.0 {
|
||
surface = mt::Invertible::inverse(&surface);
|
||
}
|
||
}
|
||
Ok(surface)
|
||
};
|
||
// Contact curves and blend faces per edge.
|
||
// (None: the planar side's contact along a pivot, a point.)
|
||
let mut c1: Vec<Option<mt::Edge>> = Vec::with_capacity(n);
|
||
let mut c2: Vec<Option<mt::Edge>> = Vec::with_capacity(n);
|
||
let mut blends: Vec<mt::Surface> = Vec::with_capacity(n);
|
||
for (k, list) in esec.iter().enumerate() {
|
||
let k1 = (k + 1) % nv;
|
||
let (a1, b1, a2, b2) = (v1.get(k), v1.get(k1), v2.get(k), v2.get(k1));
|
||
let (Some(a1), Some(b1), Some(a2), Some(b2)) = (a1, b1, a2, b2) else { return Err(fail("vertex")) };
|
||
if let Some(pv) = pivots.get(k).copied().flatten() {
|
||
// The section at the start turned about the axis to the one at the end; the other
|
||
// side's contact is the circle its end runs on.
|
||
let (s0, s1e) = (list.first().ok_or_else(|| fail("section"))?, list.last().ok_or_else(|| fail("section"))?);
|
||
let (qa, qb) = if pv.side == 1 { (s0.q2, s1e.q2) } else { (s0.q1, s1e.q1) };
|
||
let (a, b) = (qa - pv.c, qb - pv.c);
|
||
let angle = pv.axis.dot(a.cross(b)).atan2(a.dot(b));
|
||
let (sn, cs) = (angle / 2.0).sin_cos();
|
||
let mid = pv.c + a * cs + pv.axis.cross(a) * sn + pv.axis * (pv.axis.dot(a) * (1.0 - cs));
|
||
let surface = section_controls(s0, round).and_then(|ctrl| revolve(ctrl, pv.c, pv.axis, angle)).ok_or_else(|| fail("pivot"))?;
|
||
let (from, to) = if pv.side == 1 { (a2, b2) } else { (a1, b1) };
|
||
let arc = builder::circle_arc(from, to, p3(mid));
|
||
let (e1, e2) = if pv.side == 1 { (None, Some(arc)) } else { (Some(arc), None) };
|
||
c1.push(e1);
|
||
c2.push(e2);
|
||
blends.push(oriented(surface, list)?);
|
||
continue;
|
||
}
|
||
let fit = fits.get(k).and_then(Option::as_ref).ok_or_else(|| fail("fit"))?;
|
||
let [cv1, cv2] = &fit.contacts;
|
||
// 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);
|
||
}
|
||
}
|
||
c1.push(Some(mt::Edge::new(a1, b1, cv1)));
|
||
c2.push(Some(mt::Edge::new(a2, b2, cv2)));
|
||
blends.push(oriented(fit.surface.clone(), list)?);
|
||
}
|
||
// Rebuild the side faces: chain edges become contact curves, seams start at contacts.
|
||
let mut subst: HashMap<mt::EdgeID, (Option<mt::Edge>, Option<mt::Edge>)> = HashMap::new();
|
||
for (k, e) in chain.iter().enumerate() {
|
||
let (Some(x), Some(y)) = (c1.get(k), c2.get(k)) else { return Err(fail("contact")) };
|
||
subst.insert(e.id(), (x.clone(), y.clone()));
|
||
}
|
||
// Seams get new ends at the contacts (an edge between two chain vertices at both).
|
||
let mut ends: HashMap<mt::EdgeID, (mt::Edge, Option<mt::Vertex>, Option<mt::Vertex>)> = HashMap::new();
|
||
for (k, sm) in seams.iter().enumerate().flat_map(|(k, l)| l.iter().map(move |x| (k, x))) {
|
||
let (side, seam) = sm;
|
||
let v = verts.get(k).cloned().ok_or_else(|| fail("vertex"))?;
|
||
let nv = if *side == 1 { v1.get(k) } else { v2.get(k) }.ok_or_else(|| fail("vertex"))?.clone();
|
||
let slot = ends.entry(seam.id()).or_insert_with(|| (seam.clone(), None, None));
|
||
if *seam.front() == v {
|
||
slot.1 = Some(nv);
|
||
} else {
|
||
slot.2 = Some(nv);
|
||
}
|
||
}
|
||
let mut seam_subst: HashMap<mt::EdgeID, mt::Edge> = HashMap::new();
|
||
for (k, seam, _) in &split_caps {
|
||
let old = verts.get(*k).ok_or_else(|| fail("end vertex"))?;
|
||
let v = split_arcs.get(k).and_then(|es| es.first()).map(|e| e.back()).ok_or_else(|| fail("end seam"))?;
|
||
seam_subst.insert(seam.id(), crate::blend::retarget(seam, old, v, fit_tolerance)?);
|
||
}
|
||
for (id, (seam, a, b)) in ends {
|
||
if consumed.contains_key(&id) {
|
||
continue;
|
||
}
|
||
// At an end of an open chain the blend may stop past the seam's old end (a slanted end
|
||
// face, or one turning back past the side, as a box side beside a boss joined to it):
|
||
// 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
|
||
&& end_faces.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;
|
||
}
|
||
// The new ends must stay on the seam, in order, short of each other.
|
||
let (p0, p1) = (from_p3(seam.front().point()), from_p3(seam.back().point()));
|
||
let at = |v: &mt::Vertex| {
|
||
let q = from_p3(v.point());
|
||
let d = p1 - p0;
|
||
((q - p0).dot(d) / d.len2().max(1e-300), (q - p0 - d * ((q - p0).dot(d) / d.len2().max(1e-300))).len())
|
||
};
|
||
let (ta, oa) = a.as_ref().map(at).unwrap_or((0.0, 0.0));
|
||
let (tb, ob) = b.as_ref().map(at).unwrap_or((1.0, 0.0));
|
||
if oa > size * 1e-5 || ob > size * 1e-5 || ta < -1e-9 || tb > 1.0 + 1e-9 || tb - ta < 1e-6 {
|
||
return Err(too_large("the blend is larger than the neighbouring faces"));
|
||
}
|
||
let a = a.unwrap_or_else(|| seam.front().clone());
|
||
let b = b.unwrap_or_else(|| seam.back().clone());
|
||
seam_subst.insert(id, builder::line(&a, &b));
|
||
}
|
||
let mut out: Vec<mt::Face> = Vec::new();
|
||
let mut rec = Rebuild::default();
|
||
rec.pieces(&seam_subst);
|
||
for (fi, f) in faces.iter().enumerate() {
|
||
let corners: Vec<usize> = end_faces.iter().filter(|(_, g)| *g == fi).map(|(k, _)| *k).collect();
|
||
if !corners.is_empty() {
|
||
if corners.iter().any(|k| split_arcs.contains_key(k)) {
|
||
let connectors: Vec<mt::Edge> = corners
|
||
.iter()
|
||
.flat_map(|k| split_arcs.get(k).cloned().unwrap_or_else(|| arcs.get(*k).cloned().into_iter().collect()))
|
||
.collect();
|
||
let rebuilt = crate::blend::rebuild_face(f, &seam_subst, &connectors)?;
|
||
rec.face(&rebuilt, f);
|
||
out.push(rebuilt);
|
||
continue;
|
||
}
|
||
// The end face (one or both ends of the chain): its two edges at a chain end now
|
||
// stop at the contacts, with the blend's end arc between them.
|
||
let mut wires = Vec::new();
|
||
for w in f.absolute_boundaries() {
|
||
let mut es: Vec<mt::Edge> = Vec::new();
|
||
for e in w.edge_iter() {
|
||
let ne = match seam_subst.get(&e.id()) {
|
||
Some(s) => {
|
||
if e.front() == e.absolute_front() {
|
||
s.clone()
|
||
} else {
|
||
s.inverse()
|
||
}
|
||
}
|
||
None => e.clone(),
|
||
};
|
||
// 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));
|
||
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 = if used_up { e.front() } else { ne.back() };
|
||
let corner = if to == a1 {
|
||
arc.clone()
|
||
} else if to == a2 {
|
||
arc.inverse()
|
||
} else {
|
||
return Err(fail("end face corner"));
|
||
};
|
||
es.push(corner);
|
||
}
|
||
}
|
||
wires.push(mt::Wire::from(es));
|
||
}
|
||
let rebuilt = crate::heal::absolute_face(f, wires).ok_or_else(|| fail("an end face could not be rebuilt"))?;
|
||
rec.face(&rebuilt, f);
|
||
out.push(rebuilt);
|
||
continue;
|
||
}
|
||
let on1 = side1.contains(&fi);
|
||
let on2 = side2.contains(&fi);
|
||
if !on1 && !on2 && !f.edge_iter().any(|e| seam_subst.contains_key(&e.id())) {
|
||
out.push(f.clone());
|
||
continue;
|
||
}
|
||
let mut wires = Vec::new();
|
||
for w in f.absolute_boundaries() {
|
||
let mut es = Vec::new();
|
||
for e in w.edge_iter() {
|
||
let ne = if let Some((x, y)) = subst.get(&e.id()) {
|
||
// A point along a pivot: the contacts beside it meet there.
|
||
let Some(base) = (if on1 { x } else { y }) else { continue };
|
||
// Same direction as the chain edge, or reversed.
|
||
let chain_e = chain.iter().find(|c| c.id() == e.id()).ok_or_else(|| fail("edge"))?;
|
||
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()
|
||
};
|
||
es.push(ne);
|
||
}
|
||
if collapsed_boundary(&es, fit_tolerance) {
|
||
return Err(too_large("the blend would consume a neighbouring face"));
|
||
}
|
||
wires.push(mt::Wire::from(es));
|
||
}
|
||
let rebuilt = crate::heal::absolute_face(f, wires).ok_or_else(|| fail("a side face could not be rebuilt"))?;
|
||
rec.face(&rebuilt, f);
|
||
out.push(rebuilt);
|
||
}
|
||
// Blend faces: each runs its side-1 contact the other way from side 1's face.
|
||
for (k, bf) in blends.iter().enumerate() {
|
||
let (Some(e1), Some(e2), Some(ak), Some(ak1)) = (c1.get(k), c2.get(k), arcs.get(k), arcs.get((k + 1) % nv)) else {
|
||
return Err(fail("blend"));
|
||
};
|
||
let f1i = side1.get(k).ok_or_else(|| fail("side"))?;
|
||
let f1 = faces.get(*f1i).ok_or_else(|| fail("side"))?;
|
||
let chain_e = chain.get(k).ok_or_else(|| fail("edge"))?;
|
||
let used_fwd = f1
|
||
.boundary_iters()
|
||
.into_iter()
|
||
.flatten()
|
||
.find(|e| e.id() == chain_e.id())
|
||
.map(|e| e.front() == chain_e.front())
|
||
.ok_or_else(|| fail("edge use"))?;
|
||
// Side 1 runs the contact forward (as the chain) when it ran the edge forward.
|
||
// A pivot's planar contact is a point: three sides.
|
||
let edges = if used_fwd {
|
||
[e1.as_ref().map(|e| e.inverse()), Some(ak.clone()), e2.clone(), Some(ak1.inverse())]
|
||
} else {
|
||
[e1.clone(), Some(ak1.clone()), e2.as_ref().map(|e| e.inverse()), Some(ak.inverse())]
|
||
}
|
||
.into_iter()
|
||
.flatten()
|
||
.collect::<Vec<_>>();
|
||
let wire: mt::Wire = edges
|
||
.into_iter()
|
||
.flat_map(|e| {
|
||
let split = [(k, ak), ((k + 1) % nv, ak1)]
|
||
.into_iter()
|
||
.find(|(_, arc)| arc.id() == e.id())
|
||
.and_then(|(i, arc)| split_arcs.get(&i).map(|parts| (arc, parts)));
|
||
match split {
|
||
Some((arc, parts)) if e.orientation() == arc.orientation() => parts.clone(),
|
||
Some((_, parts)) => parts.iter().rev().map(|p| p.inverse()).collect(),
|
||
None => vec![e],
|
||
}
|
||
})
|
||
.collect();
|
||
let face = mt::Face::try_new(vec![wire], bf.clone()).map_err(|e| fail(&e.to_string()))?;
|
||
rec.blend(&face, [chain_e.id()]);
|
||
out.push(face);
|
||
}
|
||
let mut shells = solid.boundaries().clone();
|
||
if let Some(sh) = shells.get_mut(si) {
|
||
*sh = out.into();
|
||
}
|
||
Ok((Solid::try_new(shells).map_err(|e| fail(&format!("invalid solid: {e}")))?, rec))
|
||
})
|
||
}
|
||
|
||
#[path = "curvecorner.rs"]
|
||
mod corner;
|
||
pub(crate) use corner::corner_blend;
|
||
|
||
#[cfg(test)]
|
||
#[path = "curveblend_tests.rs"]
|
||
mod tests;
|