Class-A surfacing 3/3: blend curves, curved-edge fillets, the G2 corner and continuity analysis (#1642)

Thanks @midasdf.
This commit is contained in:
midasdf authored and GitHub committed 2026-10-16 06:26:27 -04:00
1 parent 3126962cd4
commit bbd4f29ebb
43 files changed
+4753 -132

No files matched your search

+2 -2
View File
@@ -153,7 +153,7 @@ pub fn spline_point(p: &[Vec2], control: bool, degree: u8, t: f64) -> Vec2 {
return p.first().copied().unwrap_or_default();
}
if control {
return bspline_at(p, degree.clamp(1, 7) as usize, t);
return bspline_at(p, degree.clamp(1, solidcraft_geom::MAX_BEZIER_DEGREE as u8) as usize, t);
}
let spans = fit_spans(p);
let n = spans.len().max(1);
@@ -292,7 +292,7 @@ pub fn spline_polyline(p: &[Vec2], control: bool, degree: u8) -> Vec<Vec2> {
return p.to_vec();
}
if control {
return bspline_polyline(p, degree.clamp(1, 7) as usize);
return bspline_polyline(p, degree.clamp(1, solidcraft_geom::MAX_BEZIER_DEGREE as u8) as usize);
}
let mut out = Vec::new();
for (p0, p1, t0, t1) in fit_spans(p) {
+3 -1
View File
@@ -137,7 +137,9 @@ impl H {
let (points, weights) = match *seg {
Seg2::Line { a, b } => (vec![a, b], vec![1.0; 2]),
Seg2::Cubic { p0, p1, p2, p3 } => (vec![p0, p1, p2, p3], vec![1.0; 4]),
Seg2::Bezier { n, p } if (1..=7).contains(&n) => (p[..=usize::from(n)].to_vec(), vec![1.0; usize::from(n) + 1]),
Seg2::Bezier { n, p } if (1..=solidcraft_geom::MAX_BEZIER_DEGREE as u8).contains(&n) => {
(p[..=usize::from(n)].to_vec(), vec![1.0; usize::from(n) + 1])
}
Seg2::Conic { a, apex, b, w } if w.is_finite() && w > 0.0 => (vec![a, apex, b], vec![1.0, w, 1.0]),
_ => return Err(bad("unsupported exact free-form offset piece")),
};
+1 -1
View File
@@ -256,7 +256,7 @@ impl LinkGeom {
(!*periodic || (!*control && *degree == 3 && crate::periodic_beziers(pts).is_ok()))
&& (2..=crate::curves::MAX_SPLINE_POINTS).contains(&pts.len())
&& pts.iter().all(ok)
&& (1..=7).contains(degree)
&& (1..=solidcraft_geom::MAX_BEZIER_DEGREE as u8).contains(degree)
}
}
}
+47 -1
View File
@@ -1,6 +1,6 @@
use std::f64::consts::{FRAC_PI_2, PI};
use solidcraft_geom::Vec2;
use solidcraft_geom::{Seg2, Vec2};
use crate::*;
@@ -921,3 +921,49 @@ fn projected_boundary_keeps_its_pieces_when_only_stations_go() {
assert!(sk.update_link(&j, &smaller).unwrap());
assert_eq!(sk.link_curves(&j).len(), 4);
}
#[test]
fn a_degree_nine_control_spline_stays_exact_and_closes_a_profile_exactly() {
// A G4 blend curve is one degree-9 Bézier; a sketch keeps it as a control spline whose exact
// piece is that Bézier, and profile measurement must keep using the curve, not its chords.
let mut sk = Sketch::new();
let bump =
[v(0.0, 0.0), v(1.5, 4.0), v(3.0, 7.0), v(5.0, 9.5), v(7.5, 11.0), v(12.5, 11.0), v(15.0, 9.5), v(17.0, 7.0), v(18.5, 4.0), v(20.0, 0.0)];
let pts: Vec<usize> = bump.iter().map(|q| sk.add_point(*q, None).unwrap()).collect();
let s = sk.add_curve(CurveKind::Spline { pts: pts.clone(), control: true, degree: 9, periodic: false }, None).unwrap();
let pieces = sk.exact_pieces(s);
assert_eq!(pieces.len(), 1, "{pieces:?}");
let Seg2::Bezier { n, p } = pieces[0] else { panic!("{:?}", pieces[0]) };
assert_eq!(n, 9, "the highest degree a sketch piece can carry");
for (i, q) in bump.iter().enumerate() {
assert!(p[i].dist(*q) < 1e-12, "control {i}");
}
// Evaluation and the exact piece agree at the spline's real degree (a degree clamp would show
// here), and a line's own exact piece still comes back as a line.
let exact = pieces[0];
for k in 0..=10 {
let t = k as f64 / 10.0;
assert!(crate::curves::spline_point(&bump, true, 9, t).dist(exact.point_at(t)) < 1e-9, "t={t}");
}
let l = sk.add_line(v(-5.0, 0.0), v(-2.0, 1.0), None, None, None).unwrap();
assert!(matches!(sk.exact_pieces(l).as_slice(), [Seg2::Line { .. }]));
// Close the loop: the region's area is the shoelace integral of the exact pieces, which a fine
// sampling of the piece reproduces (a chord approximation would miss the bump's sagitta).
sk.add_line(v(20.0, 0.0), v(0.0, 0.0), Some(pts[9]), Some(pts[0]), None).unwrap();
let ps = find_profiles(&sk);
assert_eq!(ps.len(), 1, "{ps:?}");
let n = 20_000;
let mut sampled = 0.0;
for i in 0..n {
let (t0, t1) = (i as f64 / n as f64, (i + 1) as f64 / n as f64);
let (a, b) = (exact.point_at(t0), exact.point_at(t1));
sampled += 0.5 * a.cross(b);
}
sampled -= 0.5 * v(20.0, 0.0).cross(v(0.0, 0.0));
assert!((ps[0].area - sampled.abs()).abs() < 1e-5, "{} vs {sampled}", ps[0].area);
assert!((ps[0].area - 100.0).abs() > 1e-3, "the region is not its chord: {}", ps[0].area);
// A degree above the cap is still refused.
let mut high: Vec<usize> = pts.clone();
high.push(sk.add_point(v(21.0, -2.0), None).unwrap());
assert!(sk.add_curve(CurveKind::Spline { pts: high, control: true, degree: 10, periodic: false }, None).is_err());
}