mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
kernel: T-spline sheets as record faces; NURBS faces on the record
surfaces.rs gains the record pieces NURBS faces are built from: a NURBS surface and curve as record geometry, a pcurve along a parameter line, a whole tensor face bounded by its net's boundary rows and columns (exact for a clamped surface), and all_new for a face new to an operation. tspline_sheet makes a regular T-spline that tensor face and an irregular one its Bézier elements, each side split at the T-points, each edge an exact Bézier piece of its side with a straight pcurve on every patch it bounds, shared between neighbours. Tests ported to the record. (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:
@@ -19,6 +19,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
use solidcraft_geom::brep::eval::CurveEval;
|
||||
use solidcraft_geom::brep::{self as b, Brep, Curve2};
|
||||
use solidcraft_geom::bspline::{HPoint, NurbsSurface};
|
||||
use solidcraft_geom::{Plane, Region2, Seg2, Vec2, Vec3};
|
||||
|
||||
use crate::body::Body;
|
||||
@@ -354,6 +355,15 @@ pub(crate) fn surface_body(r: &Brep) -> Result<Body> {
|
||||
record_body(r).map(|(body, _)| body.without_history())
|
||||
}
|
||||
|
||||
/// `body` (made by an operation on `operands`) with the history that every face and edge is new to
|
||||
/// it.
|
||||
pub(crate) fn all_new(body: Body, operands: &[&Body]) -> Result<Body> {
|
||||
let r = crate::native::brep_record(&body)?;
|
||||
let faces = vec![record::generated(Vec::new()); r.faces.len()];
|
||||
let h = record_history(&r, operands.len(), faces, |_, _, _| Vec::new());
|
||||
body.with_history_counts(h, &record::counts(operands))
|
||||
}
|
||||
|
||||
/// The faces `picked` of `body` (its record `r`; each with whether to turn it over) as their own
|
||||
/// body, each the body's face (Kept), every edge the body's.
|
||||
fn taken(body: &Body, r: &Brep, picked: &[(usize, bool)]) -> Result<Body> {
|
||||
@@ -364,6 +374,73 @@ fn taken(body: &Body, r: &Brep, picked: &[(usize, bool)]) -> Result<Body> {
|
||||
with_history(result, same, h, &record::counts(&[body]))
|
||||
}
|
||||
|
||||
// ---- NURBS faces --------------------------------------------------------------------------
|
||||
|
||||
/// A NURBS surface as a record surface (a plain B-spline where every weight is 1).
|
||||
pub(crate) fn spline_surface(n: &NurbsSurface) -> b::Surface {
|
||||
let [nu, nv] = n.count();
|
||||
let [ku, kv] = n.knots().clone();
|
||||
if n.homogeneous().iter().all(|h| h.w == 1.0) {
|
||||
let points = (0..nu).map(|i| (0..nv).map(|j| a3(n.point(i, j).xyz())).collect()).collect();
|
||||
return b::Surface::Spline { u_knots: ku, v_knots: kv, points };
|
||||
}
|
||||
let homogeneous = (0..nu).map(|i| (0..nv).map(|j| n.point(i, j)).map(|h| [h.x, h.y, h.z, h.w]).collect()).collect();
|
||||
b::Surface::RationalSpline { u_knots: ku, v_knots: kv, homogeneous }
|
||||
}
|
||||
|
||||
/// The NURBS curve of homogeneous control points `ctrl` on `knots` as a record curve, and its
|
||||
/// parameter interval.
|
||||
pub(crate) fn spline_curve(ctrl: &[HPoint], knots: Vec<f64>) -> Result<(b::Curve, [f64; 2])> {
|
||||
let degree = knots.len().checked_sub(ctrl.len() + 1).filter(|d| *d >= 1).ok_or_else(|| invalid("a spline's knots"))?;
|
||||
let interval = [*knots.get(degree).ok_or_else(|| invalid("knots"))?, *knots.get(ctrl.len()).ok_or_else(|| invalid("knots"))?];
|
||||
let curve = if ctrl.iter().all(|h| h.w == 1.0) {
|
||||
b::Curve::Spline { knots, points: ctrl.iter().map(|h| a3(h.xyz())).collect() }
|
||||
} else {
|
||||
b::Curve::RationalSpline { knots, homogeneous: ctrl.iter().map(|h| [h.x, h.y, h.z, h.w]).collect() }
|
||||
};
|
||||
Ok((curve, interval))
|
||||
}
|
||||
|
||||
/// The pcurve of an edge along a straight line of its face's parameters: from `a` at the
|
||||
/// interval's start to `z` at its end.
|
||||
pub(crate) fn uv_line(a: [f64; 2], z: [f64; 2], [t0, t1]: [f64; 2]) -> Curve2 {
|
||||
Curve2::Spline { knots: vec![t0, t0, t1, t1], points: vec![a, z] }
|
||||
}
|
||||
|
||||
/// One face: the whole of the NURBS surface `n`, bounded by its four boundary curves (each the
|
||||
/// boundary row or column of its net on its knots, exact for a clamped surface), as a surface
|
||||
/// body with no history.
|
||||
pub(crate) fn tensor_sheet(n: &NurbsSurface) -> Result<Body> {
|
||||
let [nu, nv] = n.count();
|
||||
if nu < 2 || nv < 2 {
|
||||
return Err(invalid("a face needs at least 2 x 2 control points"));
|
||||
}
|
||||
let [ku, kv] = n.knots().clone();
|
||||
let [[u0, u1], [v0, v1]] = n.domain();
|
||||
let corner = |i: usize, j: usize| n.point(i, j).project().ok_or_else(|| failed("a corner of the surface has no position"));
|
||||
let corners = [corner(0, 0)?, corner(nu - 1, 0)?, corner(nu - 1, nv - 1)?, corner(0, nv - 1)?];
|
||||
let mut parts = Parts { tolerance: tolerance_of(corners.into_iter()), ..Parts::default() };
|
||||
parts.vertices = corners.iter().map(|p| b::Vertex { point: a3(*p), tolerance: parts.tolerance }).collect();
|
||||
let row = |j: usize| (0..nu).map(|i| n.point(i, j)).collect::<Vec<_>>();
|
||||
let column = |i: usize| (0..nv).map(|j| n.point(i, j)).collect::<Vec<_>>();
|
||||
// Bottom (v0) and top (v1) run along u, left (u0) and right (u1) along v; the loop goes bottom,
|
||||
// right, top backwards, left backwards: counter-clockwise in (u, v).
|
||||
let sides = [
|
||||
(row(0), ku.clone(), [0, 1], [[u0, v0], [u1, v0]], true),
|
||||
(column(nu - 1), kv.clone(), [1, 2], [[u1, v0], [u1, v1]], true),
|
||||
(row(nv - 1), ku, [3, 2], [[u0, v1], [u1, v1]], false),
|
||||
(column(0), kv, [0, 3], [[u0, v0], [u0, v1]], false),
|
||||
];
|
||||
let mut uses = Vec::with_capacity(4);
|
||||
for (i, (ctrl, knots, [a, z], [pa, pz], forward)) in sides.into_iter().enumerate() {
|
||||
let (curve, interval) = spline_curve(&ctrl, knots)?;
|
||||
parts.edges.push(b::Edge { curve, interval, vertices: [index(a)?, index(z)?], tolerance: parts.tolerance, uses: Vec::new() });
|
||||
uses.push(b::OrientedEdge { edge: index(i)?, forward, pcurve: uv_line(pa, pz, interval) });
|
||||
}
|
||||
parts.faces.push((b::Face { surface: spline_surface(n), domain: n.domain(), loops: vec![b::Loop::Edges { edges: uses }] }, true));
|
||||
surface_body(&parts.canonical()?.brep)
|
||||
}
|
||||
|
||||
// ---- Patches ----------------------------------------------------------------------------
|
||||
|
||||
/// A planar surface filling a sketch region. Its history has no operands: the face is new.
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
//! faces. A T-mesh without T-junctions becomes one clamped tensor face; otherwise each Bézier
|
||||
//! element is a face, and the faces share their edges and vertices — a side that meets two
|
||||
//! neighbours is split at the T-point between them — so the sheet is one sewn open shell.
|
||||
//!
|
||||
//! Every edge is an exact piece of its patch's side (a Bézier segment of the side's control
|
||||
//! points) and its pcurve on each face it bounds a straight line in that patch's parameters.
|
||||
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use solidcraft_geom::brep as b;
|
||||
use solidcraft_geom::bspline::{BezierPatch, HPoint, NurbsSurface, SplineError, bezier_segment};
|
||||
use solidcraft_geom::tspline::TSpline;
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::body::Body;
|
||||
use crate::spline_conv::{h4, nurbs_curve, nurbs_surface};
|
||||
use crate::{KernelError, Result, guard};
|
||||
use crate::surfaces::{Parts, spline_curve, spline_surface, surface_body, uv_line};
|
||||
use crate::{KernelError, Result};
|
||||
|
||||
/// A T-spline as a surface body (an open shell of NURBS faces whose normals follow
|
||||
/// `∂S/∂s × ∂S/∂t`).
|
||||
pub fn tspline_sheet(ts: &TSpline) -> Result<Body> {
|
||||
let faces = if ts.mesh().is_regular() {
|
||||
if ts.mesh().is_regular() {
|
||||
let n = ts.to_nurbs().and_then(|n| n.clamped()).map_err(spline_err)?;
|
||||
guard("t-spline sheet", || tensor_face(&n).map(|f| vec![f]))?
|
||||
} else {
|
||||
let patches = ts.bezier_elements().map_err(spline_err)?;
|
||||
guard("t-spline sheet", || patch_faces(&patches))?
|
||||
};
|
||||
Ok(crate::surfaces::surface_body(faces))
|
||||
return crate::surfaces::tensor_sheet(&n);
|
||||
}
|
||||
let patches = ts.bezier_elements().map_err(spline_err)?;
|
||||
surface_body(&patch_faces(&patches)?.canonical()?.brep)
|
||||
}
|
||||
|
||||
fn spline_err(e: SplineError) -> KernelError {
|
||||
@@ -35,14 +35,8 @@ fn fail(m: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("t-spline sheet: {m}"))
|
||||
}
|
||||
|
||||
fn vertex(h: HPoint) -> Result<mt::Vertex> {
|
||||
let p = h.project().ok_or_else(|| fail("a corner has no position"))?;
|
||||
Ok(mt::builder::vertex(crate::body::p3(p)))
|
||||
}
|
||||
|
||||
/// One face from a clamped tensor surface, bounded by its four boundary curves.
|
||||
fn tensor_face(n: &NurbsSurface) -> Result<mt::Face> {
|
||||
crate::spline_conv::tensor_face(n)
|
||||
fn index(i: usize) -> Result<u32> {
|
||||
u32::try_from(i).map_err(|_| KernelError::TooLarge("t-spline sheet: too many patches".into()))
|
||||
}
|
||||
|
||||
/// A parameter point, exact (knot values are shared, so equal points have equal bits).
|
||||
@@ -52,9 +46,13 @@ fn key(s: f64, t: f64) -> Key {
|
||||
((s + 0.0).to_bits(), (t + 0.0).to_bits())
|
||||
}
|
||||
|
||||
/// One face per Bézier patch, sharing vertices and edges; sides are split where a
|
||||
/// neighbour's corner lands on them.
|
||||
fn patch_faces(patches: &[BezierPatch]) -> Result<Vec<mt::Face>> {
|
||||
fn st(k: Key) -> [f64; 2] {
|
||||
[f64::from_bits(k.0), f64::from_bits(k.1)]
|
||||
}
|
||||
|
||||
/// One face per Bézier patch, sharing vertices and edges; sides are split where a neighbour's
|
||||
/// corner lands on them.
|
||||
fn patch_faces(patches: &[BezierPatch]) -> Result<Parts> {
|
||||
// Corner parameters on each line of constant t (their s values) and of constant s.
|
||||
let mut on_t: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
|
||||
let mut on_s: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
|
||||
@@ -67,14 +65,21 @@ fn patch_faces(patches: &[BezierPatch]) -> Result<Vec<mt::Face>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut verts: HashMap<Key, mt::Vertex> = HashMap::new();
|
||||
let mut edges: HashMap<(Key, Key), mt::Edge> = HashMap::new();
|
||||
let mut faces = Vec::with_capacity(patches.len());
|
||||
let mut parts = Parts::default();
|
||||
let mut verts: HashMap<Key, u32> = HashMap::new();
|
||||
// Each edge by its ends' parameters, in the direction it was made.
|
||||
let mut edges: HashMap<(Key, Key), u32> = HashMap::new();
|
||||
let mut corners: Vec<[f64; 3]> = Vec::new();
|
||||
for p in patches {
|
||||
let mut wire: Vec<mt::Edge> = Vec::new();
|
||||
// The patch's own (u, v) on [0, 1]² of a global (s, t).
|
||||
let local = |k: Key| {
|
||||
let [s, t] = st(k);
|
||||
[(s - p.s[0]) / (p.s[1] - p.s[0]), (t - p.t[0]) / (p.t[1] - p.t[0])]
|
||||
};
|
||||
let mut uses: Vec<b::OrientedEdge> = Vec::new();
|
||||
for side in 0..4 {
|
||||
// Side 0: t = t₀, 1: s = s₁, 2: t = t₁, 3: s = s₀; each runs with its parameter
|
||||
// rising, sides 2 and 3 are walked backwards.
|
||||
// Side 0: t = t0, 1: s = s1, 2: t = t1, 3: s = s0; each runs with its parameter rising,
|
||||
// sides 2 and 3 are walked backwards.
|
||||
let (along_s, fixed, [lo, hi]) = match side {
|
||||
0 => (true, p.t[0], p.s),
|
||||
1 => (false, p.s[1], p.t),
|
||||
@@ -91,39 +96,62 @@ fn patch_faces(patches: &[BezierPatch]) -> Result<Vec<mt::Face>> {
|
||||
for w in breaks.windows(2) {
|
||||
let &[x0, x1] = w else { continue };
|
||||
let (ka, kb) = (at(x0), at(x1));
|
||||
let e = if let Some(e) = edges.get(&(ka, kb)) {
|
||||
e.clone()
|
||||
} else if let Some(e) = edges.get(&(kb, ka)) {
|
||||
e.inverse()
|
||||
} else {
|
||||
let seg = bezier_segment(&ctrl, (x0 - lo) / (hi - lo), (x1 - lo) / (hi - lo)).map_err(spline_err)?;
|
||||
let (Some(&first), Some(&last)) = (seg.first(), seg.last()) else { return Err(fail("an empty side")) };
|
||||
let mut corner = |k: Key, h: HPoint| -> Result<mt::Vertex> {
|
||||
Ok(match verts.entry(k) {
|
||||
Entry::Occupied(e) => e.get().clone(),
|
||||
Entry::Vacant(e) => e.insert(vertex(h)?).clone(),
|
||||
})
|
||||
};
|
||||
let (va, vb) = (corner(ka, first)?, corner(kb, last)?);
|
||||
let curve = nurbs_curve(mt::KnotVec::bezier_knot(seg.len() - 1), &seg)?;
|
||||
let e = mt::Edge::try_new(&va, &vb, curve).map_err(fail)?;
|
||||
edges.insert((ka, kb), e.clone());
|
||||
e
|
||||
// The edge as made (a to b with its parameter rising), and whether this side runs
|
||||
// along it.
|
||||
let (edge, made) = match (edges.get(&(ka, kb)), edges.get(&(kb, ka))) {
|
||||
(Some(e), _) => (*e, (ka, kb)),
|
||||
(None, Some(e)) => (*e, (kb, ka)),
|
||||
(None, None) => {
|
||||
let seg = bezier_segment(&ctrl, (x0 - lo) / (hi - lo), (x1 - lo) / (hi - lo)).map_err(spline_err)?;
|
||||
let (Some(&first), Some(&last)) = (seg.first(), seg.last()) else { return Err(fail("an empty side")) };
|
||||
let mut corner = |k: Key, h: HPoint| -> Result<u32> {
|
||||
if let Some(v) = verts.get(&k) {
|
||||
return Ok(*v);
|
||||
}
|
||||
let q = h.project().ok_or_else(|| fail("a corner has no position"))?;
|
||||
let v = index(corners.len())?;
|
||||
corners.push([q.x, q.y, q.z]);
|
||||
verts.insert(k, v);
|
||||
Ok(v)
|
||||
};
|
||||
let vertices = [corner(ka, first)?, corner(kb, last)?];
|
||||
let degree = seg.len().saturating_sub(1);
|
||||
let knots = std::iter::repeat_n(0.0, degree + 1).chain(std::iter::repeat_n(1.0, degree + 1)).collect();
|
||||
let (curve, interval) = spline_curve(&seg, knots)?;
|
||||
let e = index(parts.edges.len())?;
|
||||
parts.edges.push(b::Edge { curve, interval, vertices, tolerance: 0.0, uses: Vec::new() });
|
||||
edges.insert((ka, kb), e);
|
||||
(e, (ka, kb))
|
||||
}
|
||||
};
|
||||
pieces.push(e);
|
||||
let pcurve = uv_line(local(made.0), local(made.1), [0.0, 1.0]);
|
||||
pieces.push(b::OrientedEdge { edge, forward: made == (ka, kb), pcurve });
|
||||
}
|
||||
if side >= 2 {
|
||||
pieces.reverse();
|
||||
pieces.iter_mut().for_each(|e| *e = e.inverse());
|
||||
pieces.iter_mut().for_each(|u| u.forward = !u.forward);
|
||||
}
|
||||
wire.extend(pieces);
|
||||
uses.extend(pieces);
|
||||
}
|
||||
let [ps, pt] = p.degree;
|
||||
let rows: Vec<Vec<mt::Vector4>> = (0..=ps).map(|i| (0..=pt).map(|j| h4(p.point(i, j))).collect()).collect();
|
||||
let surf = nurbs_surface((mt::KnotVec::bezier_knot(ps), mt::KnotVec::bezier_knot(pt)), rows)?;
|
||||
faces.push(mt::Face::try_new(vec![wire.into()], surf).map_err(fail)?);
|
||||
let knots = |d: usize| std::iter::repeat_n(0.0, d + 1).chain(std::iter::repeat_n(1.0, d + 1)).collect::<Vec<_>>();
|
||||
let ctrl: Vec<HPoint> = (0..=pt).flat_map(|j| (0..=ps).map(move |i| (i, j))).map(|(i, j)| p.point(i, j)).collect();
|
||||
let surface = NurbsSurface::from_homogeneous([ps, pt], [knots(ps), knots(pt)], ctrl).map_err(spline_err)?;
|
||||
parts.faces.push((
|
||||
b::Face { surface: spline_surface(&surface), domain: [[0.0, 1.0], [0.0, 1.0]], loops: vec![b::Loop::Edges { edges: uses }] },
|
||||
true,
|
||||
));
|
||||
}
|
||||
Ok(faces)
|
||||
let mut bounds = solidcraft_geom::Aabb3::EMPTY;
|
||||
for c in &corners {
|
||||
bounds.add(solidcraft_geom::Vec3::new(c[0], c[1], c[2]));
|
||||
}
|
||||
parts.tolerance = (bounds.diagonal() * 1e-6).max(1e-5);
|
||||
parts.vertices = corners.into_iter().map(|point| b::Vertex { point, tolerance: parts.tolerance }).collect();
|
||||
for e in &mut parts.edges {
|
||||
e.tolerance = parts.tolerance;
|
||||
}
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -131,7 +159,6 @@ mod tests {
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_geom::bspline::NurbsSurface;
|
||||
use solidcraft_geom::tspline::{Cut, TSpline};
|
||||
use truck_modeling as mt;
|
||||
|
||||
use super::tspline_sheet;
|
||||
use crate::fuzz::Rng;
|
||||
@@ -187,8 +214,11 @@ mod tests {
|
||||
assert!(body.is_surface());
|
||||
assert_eq!(body.shell_count(), 1);
|
||||
assert_eq!(body.face_count(), faces);
|
||||
let shell = &body.solid.boundaries()[0];
|
||||
assert_eq!(shell.shell_condition(), mt::ShellCondition::Oriented, "neighbours share edges, run opposite ways");
|
||||
let record = crate::native::brep_record(&body).unwrap();
|
||||
assert!(
|
||||
record.edges.iter().all(|e| e.uses.len() == 1 || (e.uses.len() == 2 && e.uses[0].upper != e.uses[1].upper)),
|
||||
"neighbours share edges, run opposite ways"
|
||||
);
|
||||
let euler = body.vertex_count() as i64 - body.edge_count() as i64 + body.face_count() as i64;
|
||||
assert_eq!(euler, 1, "one sewn sheet (a disc)");
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user