* Add B-spline basis, knot insertion and NURBS surfaces to geom Freeform faces need local refinement and gap-free joins between patches with different knot vectors, which tensor B-spline grids cannot give; T-splines can, and the first user is an N-sided Boundary Fill of non-planar loops. This lays the univariate groundwork they rest on, in the L0 geometry crate so it stays independent of the B-rep kernel: - Cox-de Boor basis of one function from its local knot vector, on half-open spans with a left-limit side for the domain end, with derivatives (NURBS Book eq. 2.5, 2.9) - knot insertion into a single basis function (Sederberg et al. 2004 section 3) and Bezier extraction of one function by repeated insertion - Boehm knot insertion for curves (NURBS Book A5.1), de Casteljau evaluation and sub-segments - a rational tensor NURBS surface with evaluation, knot insertion and clamping, and a rational Bezier patch type All input is checked (finite, monotone knots, positive weights, size caps) and returns SplineError; nothing indexes unchecked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8 * Add T-spline surfaces: T-mesh, evaluation, AS check, refinement, extraction Tensor B-spline faces refine only by whole rows and columns and cannot join patches with different knot vectors without gaps. T-splines allow T-junctions in the control net, which fixes both; the first user is an N-sided Boundary Fill. This adds the maths only (no command, no change in behaviour): - T-mesh in index space with knot values per line, validated (outer rectangle, rectangular faces, no empty lines, size caps); local knot vectors read off rays from each vertex, rays leaving the mesh repeat the boundary (Sederberg et al. 2003 section 3) - rational T-spline evaluation and first derivatives for odd bi-degree, with a complete (p-1)/2-line frame and the domain inside it - T-junction extensions and the analysis-suitability check (Scott et al. 2012 section 2) - local refinement that keeps the surface exactly: requested knot lines are cut in, then every original blending function is carried as a record and split (violation 1) or the mesh gains edges (violations 2 and 3) until each record is a blending function of the refined mesh (Sederberg et al. 2004 section 3); analysis-suitable input stays analysis-suitable by carrying conflicting T-junctions across faces - NURBS <-> T-spline conversion for meshes without T-junctions, and Bezier extraction over the rectangles no blending-function knot line crosses Measured: regular T-spline vs NURBS < 1e-12, round trip exact, partition of unity with T-junctions < 1e-14, refinement deviation <= 1.4e-14 over 40 random refinements, Bezier patches vs surface < 1e-14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8 * Convert T-splines to sewn NURBS sheet bodies in the kernel A T-spline is only useful to the modeller once it is a B-rep face set. tspline_sheet builds an open shell: a mesh without T-junctions becomes one clamped NURBS face; otherwise each Bezier element is a face, and sides are split at the T-points where neighbours' corners land, so all faces share vertices and edges and the sheet is one oriented shell (V - E + F = 1). Every truck call runs under guard. No command yet; Boundary Fill will be the first caller. Tests: one face for a regular surface, sewn Bezier faces with T-junctions, tessellated points on the surface to ~1e-9 mm, and hostile control nets (coincident, 1e300, needle-thin) never panic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
7.1 KiB
Rust
149 lines
7.1 KiB
Rust
//! Conversions: a tensor NURBS surface as a T-spline and back (a T-mesh without T-junctions is
|
|
//! a tensor B-spline; 2003 §3 and §5), and Bézier extraction — the surface as rational Bézier
|
|
//! patches over the rectangles where every blending function is one polynomial.
|
|
|
|
use super::mesh::{Grid, TMesh};
|
|
use super::{Blend, TSpline, frame};
|
|
use crate::bspline::{BezierPatch, HPoint, NurbsSurface, SplineError, SplineResult, basis_bezier, invalid};
|
|
|
|
/// Most unit-edge marks and coefficient products in one Bézier extraction.
|
|
const MAX_EXTRACT_WORK: usize = 1 << 26;
|
|
|
|
impl TSpline {
|
|
/// A tensor NURBS surface of odd bi-degree as a T-spline on the full grid: column `i` of the
|
|
/// mesh is knot `i + (p+1)/2` of the knot vector, so the blending function at a vertex is
|
|
/// the B-spline basis function of that control point (rays that leave the mesh repeat the
|
|
/// boundary line, 2003 §3). Knot vectors whose first and last `(p+1)/2 + 1` knots are not
|
|
/// equal are clamped first (same surface on the same domain).
|
|
pub fn from_nurbs(n: &NurbsSurface) -> SplineResult<TSpline> {
|
|
let degree = n.degree();
|
|
if degree.iter().any(|p| p % 2 == 0) {
|
|
return Err(SplineError::Unsupported("a NURBS surface of even degree as a T-spline".into()));
|
|
}
|
|
let ends_ok = |k: &[f64], p: usize| {
|
|
let h = p.div_ceil(2);
|
|
let same = |w: Option<&[f64]>| w.is_some_and(|w| w.windows(2).all(|x| matches!(x, [a, b] if a == b)));
|
|
same(k.get(..=h)) && same(k.len().checked_sub(h + 1).and_then(|s| k.get(s..)))
|
|
};
|
|
let n = if (0..2).all(|d| ends_ok(&n.knots()[d], degree[d])) { n.clone() } else { n.clamped()? };
|
|
let lines = |d: usize| -> Vec<f64> {
|
|
let h = degree[d].div_ceil(2);
|
|
let k = &n.knots()[d];
|
|
k.get(h..k.len().saturating_sub(h)).map(<[f64]>::to_vec).unwrap_or_default()
|
|
};
|
|
let mesh = TMesh::tensor(lines(0), lines(1))?;
|
|
TSpline::from_homogeneous(mesh, degree, n.homogeneous().to_vec())
|
|
}
|
|
|
|
/// The T-spline as a tensor NURBS surface, when its mesh has no T-junctions.
|
|
pub fn to_nurbs(&self) -> SplineResult<NurbsSurface> {
|
|
if !self.mesh.is_regular() {
|
|
return Err(SplineError::Unsupported("a T-mesh with T-junctions as one tensor surface".into()));
|
|
}
|
|
let pad = |k: &[f64], p: usize| -> Vec<f64> {
|
|
let h = p.div_ceil(2);
|
|
let (first, last) = (k.first().copied().unwrap_or(0.0), k.last().copied().unwrap_or(0.0));
|
|
std::iter::repeat_n(first, h).chain(k.iter().copied()).chain(std::iter::repeat_n(last, h)).collect()
|
|
};
|
|
let g = &self.mesh.grid;
|
|
NurbsSurface::from_homogeneous(self.degree, [pad(&g.s, self.degree[0]), pad(&g.t, self.degree[1])], self.ctrl.clone())
|
|
}
|
|
|
|
/// The surface as rational Bézier patches over rectangles of the domain, inside each of
|
|
/// which every blending function is one polynomial: the knot lines of all blending
|
|
/// functions (each function's s-knots across its t-support and t-knots across its
|
|
/// s-support) are marked on the index grid and the domain's cells are gathered into
|
|
/// rectangles that no marked line crosses. Each function's Bernstein coefficients on a
|
|
/// rectangle come from knot insertion ([`basis_bezier`]). Patches tile the domain and meet
|
|
/// at T-points where their corners land on a neighbour's side.
|
|
pub fn bezier_elements(&self) -> SplineResult<Vec<BezierPatch>> {
|
|
let g = &self.mesh.grid;
|
|
let marks = knot_lines(g, &self.blends)?;
|
|
let [cs, ct] = frame(self.degree);
|
|
let (ns, nt) = (g.ns(), g.nt());
|
|
let (ci_end, cj_end) = (ns - 1 - cs, nt - 1 - ct);
|
|
let mut taken = vec![false; ns * nt];
|
|
let is_taken = |taken: &[bool], i: usize, j: usize| taken.get(j * ns + i).copied().unwrap_or(true);
|
|
let mut rects = Vec::new();
|
|
for cj in ct..cj_end {
|
|
for ci in cs..ci_end {
|
|
if is_taken(&taken, ci, cj) {
|
|
continue;
|
|
}
|
|
let mut i1 = ci + 1;
|
|
while i1 < ci_end && !is_taken(&taken, i1, cj) && !marks.v(i1, cj) {
|
|
i1 += 1;
|
|
}
|
|
let mut j1 = cj + 1;
|
|
while j1 < cj_end && (ci..i1).all(|x| !is_taken(&taken, x, j1) && !marks.h(x, j1)) && (ci + 1..i1).all(|x| !marks.v(x, j1)) {
|
|
j1 += 1;
|
|
}
|
|
for y in cj..j1 {
|
|
for x in ci..i1 {
|
|
if let Some(c) = taken.get_mut(y * ns + x) {
|
|
*c = true;
|
|
}
|
|
}
|
|
}
|
|
rects.push((ci, i1, cj, j1));
|
|
}
|
|
}
|
|
let val = |k: &[f64], i: usize| k.get(i).copied().unwrap_or(0.0);
|
|
let [ps, pt] = self.degree;
|
|
let mut work = 0usize;
|
|
let mut out = Vec::new();
|
|
for (i0, i1, j0, j1) in rects {
|
|
let (s0, s1, t0, t1) = (val(&g.s, i0), val(&g.s, i1), val(&g.t, j0), val(&g.t, j1));
|
|
if !(s0 < s1 && t0 < t1) {
|
|
continue;
|
|
}
|
|
let mut ctrl = vec![HPoint::ZERO; (ps + 1) * (pt + 1)];
|
|
for (b, c) in self.blends.iter().zip(&self.ctrl) {
|
|
let [[bs0, bs1], [bt0, bt1]] = b.support();
|
|
if !(s1.min(bs1) > s0.max(bs0) && t1.min(bt1) > t0.max(bt0)) {
|
|
continue;
|
|
}
|
|
work += ctrl.len();
|
|
if work > MAX_EXTRACT_WORK {
|
|
return Err(SplineError::TooLarge("Bézier extraction needs too much work".into()));
|
|
}
|
|
let cs = basis_bezier(&b.sv, s0, s1)?;
|
|
let ct = basis_bezier(&b.tv, t0, t1)?;
|
|
for (j, bj) in ct.iter().enumerate() {
|
|
for (i, bi) in cs.iter().enumerate() {
|
|
if let Some(slot) = ctrl.get_mut(j * (ps + 1) + i) {
|
|
*slot += *c * (bi * bj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ctrl.iter().any(|h| !h.is_finite() || !(h.w > 0.0)) {
|
|
return invalid("a Bézier patch has a non-positive weight");
|
|
}
|
|
out.push(BezierPatch { s: [s0, s1], t: [t0, t1], degree: self.degree, ctrl });
|
|
}
|
|
Ok(out)
|
|
}
|
|
}
|
|
|
|
/// The unit edges of the index grid that carry a knot line of some blending function: each
|
|
/// function's s-knots across its t-support and its t-knots across its s-support.
|
|
fn knot_lines(g: &Grid, blends: &[Blend]) -> SplineResult<Grid> {
|
|
let mut m = Grid::empty(g.s.clone(), g.t.clone())?;
|
|
let mut work = 0usize;
|
|
for b in blends {
|
|
let (Some(&s0), Some(&s1), Some(&t0), Some(&t1)) = (b.s.first(), b.s.last(), b.t.first(), b.t.last()) else { continue };
|
|
work += (b.s.len() * (t1 - t0)) + (b.t.len() * (s1 - s0));
|
|
if work > MAX_EXTRACT_WORK {
|
|
return Err(SplineError::TooLarge("Bézier extraction needs too much work".into()));
|
|
}
|
|
for &x in &b.s {
|
|
(t0..t1).for_each(|y| m.set_v(x, y));
|
|
}
|
|
for &y in &b.t {
|
|
(s0..s1).for_each(|x| m.set_h(x, y));
|
|
}
|
|
}
|
|
Ok(m)
|
|
}
|