Fillets and chamfers meeting at corners on curved faces: what #935 leaves (#854)

* Fillets and chamfers meeting at corners on curved faces, on top of #935

What #935 and #926 leave of #851 and the matrix:

- Corner build for blends meeting on faces of any surface (curvecorner.rs):
  the knife-edge tip's round with the arc's or the back edge's, edges
  picked in any order, a round ending in an earlier round of its radius
  (as a feature of its own), and chamfers meeting at the arc's corner.
  Its end curves are fitted by chord length: a uniform parameter
  through points clustered at the ends doubled back on itself there,
  and the display planner refused the face.
- A straight edge ending at a curved face (the back edge into the
  knife-edge tip): rolled, or cut by the face in the planar corner
  build (blend_corner.rs).
- A rolling ball turning about a rounded corner of its own radius
  (a pivot: a piece of sphere or cone).
- Seams that are an earlier blend's straight contact (fitted curves)
  count as straight.
- No spline across a joint where the strip's curvature jumps (a line
  into a small round): each strip ends on the joint's section.
- Sections at a chain's vertices on the faces of the edge from it
  alone.
- Every new fillet path passes #949's check for openings.

Tests measure volumes through the display plan, as #935's do.
Action matrix: 51 more fillets and chamfers pass (sweep, tangent arc
chain, sheet-metal hem, bracket holes), none fewer. Robustness seed 33
recorded (its last fillet builds differently; display and measured
volumes now agree).

Co-Authored-By: bherbruck <brennen.herbruck@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bV2ntwLfUHV4JnCMPSyRi

* Corner blend tests: scale, seed 33's volumes, tighter engine bounds

From the review of #854:

- seed_33_display_plan_and_measure_agree: seed 33's bodies measure
  the same through the display plan and the measure (2e-5), which the
  golden's re-record rests on.
- the_arc_rounded_after_the_back_edge_matches_both_together: the order
  that still leaves 0.05 mm³ at the tip, as an ignored test.
- corner_blends_hold_at_any_scale: the cases this branch adds on the
  #851 part, at 0.01x and 100x, take off what they take at full size
  times k³ (within 5e-6, checked to 1e-4).
- The engine tests on the part as drawn measure through the display
  plan; bounds are about twice what each reads (2e-4 to 3e-3), and the
  front corner's sets are checked against the kernel's CSG grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bV2ntwLfUHV4JnCMPSyRi

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 07:40:23 -04:00
1 parent 27374ca289
commit 32d3cf6877
13 files changed
+2739 -129

No files matched your search

+26 -2
View File
@@ -1332,10 +1332,34 @@ fn cubic_basis(knots: &[f64], n_ctrl: usize, u: f64) -> Vec<f64> {
/// Cubic B-spline through points at uniform parameters (clamped, knots by averaging).
pub(crate) fn interpolate_cubic(pts: &[Vec3]) -> Option<mt::BSplineCurve<mt::Point3>> {
let m = pts.len();
if m < 4 {
let u: Vec<f64> = (0..m).map(|i| i as f64 / (m.max(2) - 1) as f64).collect();
interpolate_cubic_at(pts, &u)
}
/// [`interpolate_cubic`] by chord length: the parameters (0 to 1) in proportion to the distance
/// along the points. Points closer together toward an end (where a curve leaves another
/// tangentially) then don't slow the curve to a stop there, where a uniform parameter lets it
/// double back on itself.
pub(crate) fn interpolate_cubic_chordal(pts: &[Vec3]) -> Option<(mt::BSplineCurve<mt::Point3>, Vec<f64>)> {
let mut u = vec![0.0];
for w in pts.windows(2) {
let (Some(a), Some(b)) = (w.first(), w.get(1)) else { return None };
u.push(u.last().copied().unwrap_or(0.0) + a.dist(*b));
}
let total = u.last().copied().filter(|t| t.is_finite() && *t > 0.0)?;
let u: Vec<f64> = u.iter().map(|x| x / total).collect();
if u.windows(2).any(|w| w.first() >= w.get(1)) {
return None;
}
interpolate_cubic_at(pts, &u).map(|c| (c, u))
}
/// The cubic B-spline through `pts` at parameters `u` (rising from 0 to 1), its knots averaged.
fn interpolate_cubic_at(pts: &[Vec3], u: &[f64]) -> Option<mt::BSplineCurve<mt::Point3>> {
let m = pts.len();
if m < 4 || u.len() != m {
return None;
}
let u: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
let mut knots = vec![0.0; 4];
for j in 1..(m - 3) {
knots.push((j..j + 3).filter_map(|i| u.get(i)).sum::<f64>() / 3.0);