kernel: spline_conv makes record curves, surfaces and tensor sheets

geom's NURBS curves and surfaces become the record's rational splines, weights kept.
tensor_face builds a whole tensor surface as a one-face sheet (assemble's new Assembly::sheet:
one open shell in the infinite region): the net's first and last rows and columns as edges on
the surface's own knots, sharing the corners, each with its exact pcurve (an iso-line, the
edge's parameter being the surface's along it), and returns the body.

(not built: base in progress)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 07:14:57 -04:00
parent f53405d6bb
commit bd956b88a7
2 changed files with 63 additions and 34 deletions
+12
View File
@@ -131,6 +131,18 @@ impl Assembly {
Ok(Assembly { tolerance: b.tolerance, vertices: b.vertices.clone(), edges, faces, regions: b.regions.clone(), shells: b.shells.len() })
}
/// An empty sheet to build faces into: one open shell (shell 0) in the infinite region.
pub(crate) fn sheet(tolerance: f64) -> Assembly {
Assembly {
tolerance,
vertices: Vec::new(),
edges: Vec::new(),
faces: Vec::new(),
regions: vec![Region { shells: vec![0], void: false }],
shells: 1,
}
}
/// A new vertex at `p`.
pub(crate) fn vertex(&mut self, p: Vec3) -> Result<u32> {
if !p.is_finite() {
+51 -34
View File
@@ -1,64 +1,81 @@
//! geom's B-spline curves and surfaces as truck's NURBS geometry, weights kept (homogeneous
//! control points carried over as they are).
//! geom's B-spline curves and surfaces as schema 4 records, weights kept (homogeneous control
//! points carried over as they are), and a whole tensor surface as a one-face sheet.
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::{Curve, Curve2, Surface};
use solidcraft_geom::bspline::{HPoint, NurbsSurface};
use truck_modeling as mt;
use crate::body::p3;
use crate::assemble::{AFace, AUse, Assembly};
use crate::body::Body;
use crate::{KernelError, Result};
fn fail(m: impl std::fmt::Display) -> KernelError {
KernelError::Failed(format!("spline geometry: {m}"))
}
/// A homogeneous control point as truck's.
pub(crate) fn h4(h: HPoint) -> mt::Vector4 {
mt::Vector4::new(h.x, h.y, h.z, h.w)
/// A homogeneous control point as the record's `[x*w, y*w, z*w, w]`.
pub(crate) fn h4(h: HPoint) -> [f64; 4] {
[h.x, h.y, h.z, h.w]
}
/// A NURBS curve of knots and homogeneous control points.
pub(crate) fn nurbs_curve(knots: mt::KnotVec, ctrl: &[HPoint]) -> Result<mt::Curve> {
let c = mt::BSplineCurve::try_new(knots, ctrl.iter().copied().map(h4).collect()).map_err(fail)?;
Ok(mt::Curve::NurbsCurve(mt::NurbsCurve::new(c)))
pub(crate) fn nurbs_curve(knots: &[f64], ctrl: &[HPoint]) -> Curve {
Curve::RationalSpline { knots: knots.to_vec(), homogeneous: ctrl.iter().copied().map(h4).collect() }
}
/// A NURBS surface of knots and homogeneous control points (`rows[i][j]`: i along u).
pub(crate) fn nurbs_surface(knots: (mt::KnotVec, mt::KnotVec), rows: Vec<Vec<mt::Vector4>>) -> Result<mt::Surface> {
let s = mt::BSplineSurface::try_new(knots, rows).map_err(fail)?;
Ok(mt::Surface::NurbsSurface(mt::NurbsSurface::new(s)))
pub(crate) fn nurbs_surface(u_knots: &[f64], v_knots: &[f64], rows: Vec<Vec<[f64; 4]>>) -> Surface {
Surface::RationalSpline { u_knots: u_knots.to_vec(), v_knots: v_knots.to_vec(), homogeneous: rows }
}
/// geom's tensor surface as truck's.
pub(crate) fn surface(s: &NurbsSurface) -> Result<mt::Surface> {
/// geom's tensor surface as a record surface.
pub(crate) fn surface(s: &NurbsSurface) -> Surface {
let [nu, nv] = s.count();
let [ku, kv] = s.knots();
let rows: Vec<Vec<mt::Vector4>> = (0..nu).map(|i| (0..nv).map(|j| h4(s.point(i, j))).collect()).collect();
nurbs_surface((mt::KnotVec::from(ku.clone()), mt::KnotVec::from(kv.clone())), rows)
nurbs_surface(ku, kv, (0..nu).map(|i| (0..nv).map(|j| h4(s.point(i, j))).collect()).collect())
}
/// One face of a whole tensor surface: its four boundary curves as edges, sharing the corners.
/// This is what a patch built as a single control net (`bridge`, `boundary`, the T-spline sheet)
/// hands to the B-rep.
pub(crate) fn tensor_face(s: &NurbsSurface) -> Result<mt::Face> {
/// The active interval `[knots[degree], knots[count]]` of a direction with `count` points.
fn active(knots: &[f64], count: usize) -> Result<[f64; 2]> {
let degree = knots.len().checked_sub(count + 1).ok_or_else(|| fail("knots and points disagree"))?;
match (knots.get(degree), knots.get(count)) {
(Some(a), Some(b)) if a < b => Ok([*a, *b]),
_ => Err(fail("an empty knot interval")),
}
}
/// One face of a whole tensor surface as a sheet: its four boundary curves (the net's first and
/// last rows and columns) as edges sharing the corners, each with its exact pcurve (an edge's
/// parameter is the surface's along it). This is what a patch built as a single control net
/// (`bridge`, `boundary`, the T-spline sheet) hands to the B-rep.
pub(crate) fn tensor_face(s: &NurbsSurface) -> Result<Body> {
let [nu, nv] = s.count();
if nu < 2 || nv < 2 {
return Err(fail("a face needs at least 2 x 2 control points"));
}
let [ku, kv] = s.knots();
let ([u0, u1], [v0, v1]) = (active(ku, nu)?, active(kv, nv)?);
let corners = [s.point(0, 0), s.point(nu - 1, 0), s.point(nu - 1, nv - 1), s.point(0, nv - 1)];
let mut v: Vec<mt::Vertex> = Vec::with_capacity(4);
for c in corners {
let p = c.project().ok_or_else(|| fail("a corner has no position"))?;
v.push(mt::builder::vertex(p3(p)));
}
let corners: Vec<Vec3> = corners.iter().map(|c| c.project().ok_or_else(|| fail("a corner has no position"))).collect::<Result<_>>()?;
let diag = corners.iter().flat_map(|a| corners.iter().map(move |b| a.dist(*b))).fold(0.0, f64::max);
let mut asm = Assembly::sheet((diag * 1e-7).max(1e-9));
let v: Vec<u32> = corners.iter().map(|p| asm.vertex(*p)).collect::<Result<_>>()?;
let (Some(&a), Some(&b), Some(&c), Some(&d)) = (v.first(), v.get(1), v.get(2), v.get(3)) else { return Err(fail("corners")) };
let row = |j: usize| -> Vec<HPoint> { (0..nu).map(|i| s.point(i, j)).collect() };
let column = |i: usize| -> Vec<HPoint> { (0..nv).map(|j| s.point(i, j)).collect() };
let (Some(v0), Some(v1), Some(v2), Some(v3)) = (v.first(), v.get(1), v.get(2), v.get(3)) else { return Err(fail("corners")) };
let edge = |a: &mt::Vertex, b: &mt::Vertex, c: mt::Curve| mt::Edge::try_new(a, b, c).map_err(fail);
let bottom = edge(v0, v1, nurbs_curve(mt::KnotVec::from(ku.clone()), &row(0))?)?;
let right = edge(v1, v2, nurbs_curve(mt::KnotVec::from(kv.clone()), &column(nu - 1))?)?;
let top = edge(v3, v2, nurbs_curve(mt::KnotVec::from(ku.clone()), &row(nv - 1))?)?;
let left = edge(v0, v3, nurbs_curve(mt::KnotVec::from(kv.clone()), &column(0))?)?;
let wire: mt::Wire = vec![bottom, right, top.inverse(), left.inverse()].into();
mt::Face::try_new(vec![wire], surface(s)?).map_err(fail)
let line = |t: [f64; 2], p: [f64; 2], q: [f64; 2]| Curve2::Spline { knots: vec![t[0], t[0], t[1], t[1]], points: vec![p, q] };
// Bottom (v0) and top (v1) run along u, right (u1) and left (u0) along v, each from its
// lower corner; the loop runs counter-clockwise in (u, v).
let bottom = asm.edge(nurbs_curve(ku, &row(0)), [u0, u1], a, b)?;
let right = asm.edge(nurbs_curve(kv, &column(nu - 1)), [v0, v1], b, c)?;
let top = asm.edge(nurbs_curve(ku, &row(nv - 1)), [u0, u1], d, c)?;
let left = asm.edge(nurbs_curve(kv, &column(0)), [v0, v1], a, d)?;
let loop_ = vec![
AUse { edge: bottom, forward: true, given: Some(line([u0, u1], [u0, v0], [u1, v0])) },
AUse { edge: right, forward: true, given: Some(line([v0, v1], [u1, v0], [u1, v1])) },
AUse { edge: top, forward: false, given: Some(line([u0, u1], [u0, v1], [u1, v1])) },
AUse { edge: left, forward: false, given: Some(line([v0, v1], [u0, v0], [u0, v1])) },
];
asm.face(AFace { surface: surface(s), natural_out: true, loops: vec![loop_], shell: 0, domain: Some([[u0, u1], [v0, v1]]) });
Body::from_record(asm.finish()?.record)
}