Blends: a curved seam where the edge chain runs on holds the contact on itself (#1291)

A round or chamfer whose edge chain runs on across a curved seam (the end circle of a lofted tube whose axis bends) is now built: the section solve keeps its contact on the seam through a plane taken afresh at the seam's nearest point at each step. Straight seams are unchanged. Robustness seed 41 is re-recorded: its second fillet, refused before, now builds. Fixes #1213.

Co-authored-by: gvocale <gvocale@users.noreply.github.com>
This commit is contained in:
Giovanniandgvocale authored and GitHub committed 2026-10-15 01:19:51 -04:00
1 parent ee9b4b9fb1
commit faeeff1c64
4 files changed
+152 -22

No files matched your search

+43 -20
View File
@@ -421,6 +421,29 @@ struct Section {
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};
@@ -449,22 +472,12 @@ 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.
/// 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<(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<Extra<'_>>, tol: f64) -> Option<Section> {
solve_from(f1, f2, p, t, r, sigma, cusp, extra, tol, None)
}
@@ -479,7 +492,7 @@ fn solve_from(
r: f64,
sigma: f64,
cusp: f64,
extra: Option<(usize, Vec3, Vec3)>,
extra: Option<Extra<'_>>,
tol: f64,
start: Option<Vec3>,
) -> Option<Section> {
@@ -558,10 +571,19 @@ fn solve_from(
// 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((k, o, m)) => {
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];
@@ -1126,15 +1148,16 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
slanted.push((k, fi, p, t, None));
}
}
let extra = match here.first().cloned().filter(|_| !at_end) {
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()))?;
if !straight(&seam, tol * 10.0) {
return Err(unsupported("a curved seam where the edge chain turns"));
}
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).
@@ -1144,7 +1167,7 @@ fn curve_blend_with_limit(solid: &Solid, ids: &[mt::EdgeID], size: f64, r: f64,
.min_by(|a, b| a.0.dist(p).total_cmp(&b.0.dist(p)))
.map(|x| x.1)
.ok_or_else(|| KernelError::Failed("seam normal".into()))?;
Some((side, o, d.cross(nn).normalized().ok_or_else(|| KernelError::Failed("seam plane".into()))?))
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