Display tessellation plans (4/4): one GPU dispatch for the scene, intersection edges by Newton (#806)

Packs the scene's planned bodies into one GPU upload and dispatch, and evaluates intersection edges by seeded Newton refinement from a fitted B-spline through exactly solved leader points, falling back to the previous evaluation near tangency. Intersection nodes carry their faces' parameters. This changes speed, not what the app draws.

Part 4 of 4 of #303.

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
This commit is contained in:
Irakli Kourtisandjyviko authored and GitHub committed 2026-10-13 22:57:08 -04:00
1 parent c1c813e572
commit 587cf865b8
9 files changed
+612 -106

No files matched your search

+119
View File
@@ -854,3 +854,122 @@ pub(crate) mod tests {
assert_eq!(local_basis(&[], 0.5, Side::Right, 1), vec![0.0, 0.0]);
}
}
/// A non-rational B-spline curve whose control points have `N` coordinates: a point in space
/// together with other values that vary along it (an intersection curve with its parameters on
/// both surfaces).
#[derive(Clone, Debug, PartialEq)]
pub struct CurveN<const N: usize> {
pub degree: usize,
pub knots: Vec<f64>,
pub ctrl: Vec<[f64; N]>,
}
impl<const N: usize> CurveN<N> {
/// The curve of degree `degree` through `points` at the increasing parameters `params`
/// (global interpolation, The NURBS Book A9.1: clamped, interior knots by averaging the
/// parameters).
pub fn interpolate(params: &[f64], points: &[[f64; N]], degree: usize) -> SplineResult<CurveN<N>> {
let m = points.len();
if params.len() != m || degree == 0 || degree > MAX_SPLINE_DEGREE || m <= degree {
return invalid(format!("{m} points, {} parameters, degree {degree}", params.len()));
}
if m > MAX_KNOTS || params.windows(2).any(|w| !(w[1] > w[0])) || params.iter().any(|t| !t.is_finite()) {
return invalid("the parameters must be finite and increasing");
}
let (first, last) = (params[0], params[m - 1]);
let mut knots = vec![first; degree + 1];
for j in 1..m - degree {
knots.push(params[j..j + degree].iter().sum::<f64>() / degree as f64);
}
knots.extend(std::iter::repeat_n(last, degree + 1));
// N · P = Q, dense with partial pivoting (the matrix is banded; m is small here).
let mut a = vec![vec![0.0f64; m]; m];
for (row, &t) in a.iter_mut().zip(params) {
let span = crate::tess::span_of(&knots, degree, m, t);
let Some(b) = crate::tess::basis_ders(&knots, span, degree, t) else { return invalid("basis") };
for (x, n) in row.iter_mut().skip(span - degree).zip(&b[0][..=degree]) {
*x = *n;
}
}
let mut q: Vec<[f64; N]> = points.to_vec();
for col in 0..m {
let Some(piv) = (col..m).max_by(|&x, &y| a[x][col].abs().total_cmp(&a[y][col].abs())) else { return invalid("empty") };
if !(a[piv][col].abs() > 1e-14) {
return invalid("singular interpolation system");
}
a.swap(col, piv);
q.swap(col, piv);
let (pr, pq) = (a[col].clone(), q[col]);
for r in col + 1..m {
let f = a[r][col] / pr[col];
if f != 0.0 {
for (x, p) in a[r].iter_mut().zip(&pr).skip(col) {
*x -= f * p;
}
for (x, p) in q[r].iter_mut().zip(pq) {
*x -= f * p;
}
}
}
}
let mut ctrl = vec![[0.0; N]; m];
for r in (0..m).rev() {
let mut v = q[r];
for (c, x) in ctrl.iter().enumerate().skip(r + 1) {
for (vi, xi) in v.iter_mut().zip(x) {
*vi -= a[r][c] * xi;
}
}
ctrl[r] = v.map(|x| x / a[r][r]);
}
if ctrl.iter().flatten().any(|x| !x.is_finite()) {
return invalid("interpolation did not give finite control points");
}
Ok(CurveN { degree, knots, ctrl })
}
/// The point and first derivative at `t` (clamped to the curve's range).
pub fn eval(&self, t: f64) -> Option<([f64; N], [f64; N])> {
let (p, n) = (self.degree, self.ctrl.len());
if n <= p || self.knots.len() != n + p + 1 || !t.is_finite() {
return None;
}
let t = t.clamp(*self.knots.get(p)?, *self.knots.get(n)?);
let span = crate::tess::span_of(&self.knots, p, n, t);
let b = crate::tess::basis_ders(&self.knots, span, p, t)?;
let (mut x, mut dx) = ([0.0; N], [0.0; N]);
let ctrl = self.ctrl.get(span - p..=span)?;
for ((c, n), dn) in ctrl.iter().zip(&b[0]).zip(&b[1]) {
for ((xk, dk), ck) in x.iter_mut().zip(dx.iter_mut()).zip(c) {
*xk += n * ck;
*dk += dn * ck;
}
}
Some((x, dx))
}
}
#[cfg(test)]
mod curve_n_tests {
use super::*;
#[test]
fn interpolates_its_points_and_their_slope() {
// A helix with a fourth coordinate rising linearly: the cubic passes through every
// point and follows the curve between them.
let at = |t: f64| [t.cos(), t.sin(), 0.3 * t, 2.0 * t];
let params: Vec<f64> = (0..=24).map(|k| f64::from(k) * 0.25).collect();
let points: Vec<[f64; 4]> = params.iter().map(|&t| at(t)).collect();
let Ok(c) = CurveN::interpolate(&params, &points, 3) else { panic!("interpolates") };
for (t, p) in params.iter().zip(&points) {
let Some((q, _)) = c.eval(*t) else { panic!("evaluates") };
assert!(q.iter().zip(p).all(|(a, b)| (a - b).abs() < 1e-9), "{t}");
}
let Some((q, d)) = c.eval(2.6) else { panic!("evaluates") };
let w = at(2.6);
assert!(q.iter().zip(w).all(|(a, b)| (a - b).abs() < 1e-3), "{q:?} vs {w:?}");
assert!((d[3] - 2.0).abs() < 1e-6 && (d[2] - 0.3).abs() < 1e-3, "{d:?}");
assert!(CurveN::interpolate(&[0.0, 0.0, 1.0, 2.0], &points[..4], 3).is_err(), "repeated parameter");
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ pub const MAX_EVAL_KNOTS: usize = 1 << 12;
/// Most control points in one evaluable surface.
pub const MAX_EVAL_POINTS: usize = 1 << 18;
/// Basis slots for the highest evaluable degree.
const W: usize = MAX_SPLINE_DEGREE + 1;
pub(crate) const W: usize = MAX_SPLINE_DEGREE + 1;
/// A rational B-spline curve: homogeneous control points `[x·w, y·w, z·w, w]`. Outside its
/// domain it continues as the polynomial of the end span (a line extends for ever).
@@ -241,7 +241,7 @@ fn nurbs_derivs(degree: [usize; 2], knots: &[Vec<f64>; 2], count: [usize; 2], ct
/// The knot span used at `t`: the last non-empty span starting at or before `t`, clamped to
/// the valid spans `p ≤ k < n` so parameters just outside the domain extend the end spans.
fn span_of(knots: &[f64], p: usize, n: usize, t: f64) -> usize {
pub(crate) fn span_of(knots: &[f64], p: usize, n: usize, t: f64) -> usize {
let mut k = p;
for i in p..n {
if let (Some(&a), Some(&b)) = (knots.get(i), knots.get(i + 1))
@@ -259,7 +259,7 @@ fn span_of(knots: &[f64], p: usize, n: usize, t: f64) -> usize {
/// Non-zero basis functions of degree `p` on span `span` and their first derivatives
/// (NURBS Book A2.3 with n = 1). `ders[0][j]` is `N_{span−p+j}`, `ders[1][j]` its derivative.
fn basis_ders(knots: &[f64], span: usize, p: usize, t: f64) -> Option<[[f64; W]; 2]> {
pub(crate) fn basis_ders(knots: &[f64], span: usize, p: usize, t: f64) -> Option<[[f64; W]; 2]> {
if p == 0 || p >= W {
return None;
}