Files
OneCAD-Draft/src/scene/convert/spline_tess.rs
T
Hakan SevenandClaude Opus 4.8 340a358c26 fix(solid3d): keep loaded-solid mesh verts f64 until the RTE split
Loaded 3DSOLID faces rendered on a ~0.06 m grid and crawled against their own
wireframe at UTM scale. Both ACIS tessellation paths cast each vertex f64→f32 in
the samplers, and only split the double-single low half (verts_low) when the
body carried a `transform` record. But many files — budweiser among them — store
solid geometry in absolute coordinates with no body transform, so verts_low
stayed empty: the GPU got position_low = 0 and reconstructed an already
f32-quantized absolute position, while the feature edges (split unconditionally
from f64) stayed exact.

Both paths now accumulate vertices in f64 and hand them to one finalize step:

  finalize_mesh(name, verts_f64, normals, indices, color, xform)

the only place a solid vertex becomes f32 — it computes the world coordinate in
f64 (body placement applied, or identity), then splits into (verts, verts_low)
unconditionally, exactly as attach_feature_edges already does for the wireframe.
push_quad / tess_plane_face / tess_cone_face / tess_sphere_face /
tess_torus_face / tess_spline_face and the truck append_group / bespoke_face /
append_spline_faces all carry f64; apply_body_transform is gone.

Verified on budweiser (all solids, body_transform = NONE): verts_low is now
populated with the captured residual, max |low| ≈ 0.0624 m. The Model-tab
(truck_tess::polygon_to_result) and edge paths were already correct and are
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:30:00 +03:00

256 lines
8.7 KiB
Rust

// B-spline (NURBS) surface tessellation for ACIS `spline-surface` faces.
//
// Lofted / swept / revolved surfaces store their geometry as an ACIS
// `nubs` (non-uniform B-spline) block inside the `spline-surface` record.
// Rather than evaluate the basis functions by hand, we parse the control net
// and knot vectors out of the SAT tokens, hand them to truck's
// `BSplineSurface` (the same NURBS kernel the Model tab already builds on),
// and sample its parametric grid into triangles.
use acadrust::entities::acis::{SatDocument, SatFace, SatRecord, SatToken};
use truck_modeling::base::{Vector3, Vector4};
use truck_modeling::{
BSplineSurface, KnotVec, NurbsSurface, ParametricSurface, ParametricSurface3D, Point3,
};
use crate::scene::convert::solid3d_tess::LodConfig;
/// A `spline-surface` block is either a non-rational `nubs` (control points are
/// plain xyz) or a rational `nurbs` (control points carry a weight). They share
/// the parametric-surface interface, so tessellation treats them uniformly.
enum SplineSurf {
Bs(BSplineSurface<Point3>),
Nurbs(NurbsSurface<Vector4>),
}
impl SplineSurf {
fn parameter_range(
&self,
) -> (
(std::ops::Bound<f64>, std::ops::Bound<f64>),
(std::ops::Bound<f64>, std::ops::Bound<f64>),
) {
match self {
SplineSurf::Bs(s) => s.parameter_range(),
SplineSurf::Nurbs(s) => s.parameter_range(),
}
}
fn subs(&self, u: f64, v: f64) -> Point3 {
match self {
SplineSurf::Bs(s) => s.subs(u, v),
SplineSurf::Nurbs(s) => s.subs(u, v),
}
}
fn normal(&self, u: f64, v: f64) -> Vector3 {
match self {
SplineSurf::Bs(s) => s.normal(u, v),
SplineSurf::Nurbs(s) => s.normal(u, v),
}
}
}
/// Tessellate one `spline-surface` face by sampling its B-spline surface.
/// Appends triangles to the shared mesh buffers; a no-op when the surface
/// record can't be parsed into a B-spline.
pub fn tess_spline_face(
sat: &SatDocument,
face: &SatFace,
lod: LodConfig,
verts: &mut Vec<[f64; 3]>,
normals: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
) {
let Some(surf_rec) = sat.resolve(face.surface()) else {
return;
};
let Some(surface) = build_spline_surface(surf_rec) else {
return;
};
// Sample over the knot domain. The B-spline patches stored by loft/sweep
// are already trimmed to the face, so the full parametric rectangle is the
// visible surface — no separate boundary trim needed.
let (u_range, v_range) = surface.parameter_range();
let (u0, u1) = range_bounds(u_range);
let (v0, v1) = range_bounds(v_range);
if !(u1 > u0) || !(v1 > v0) {
return;
}
// A B-spline patch has no single analytic radius to drive a chord-tolerance
// count, so sample at the LOD's nominal density (its unit-circle segment
// count). Floor 8 so a curved patch stays smooth.
let n = crate::scene::convert::solid3d_tess::nominal_segs(lod.chord_frac).max(8);
let (su, sv) = (n, n);
let base = verts.len() as u32;
for j in 0..=sv {
let v = v0 + (v1 - v0) * (j as f64 / sv as f64);
for i in 0..=su {
let u = u0 + (u1 - u0) * (i as f64 / su as f64);
let p = surface.subs(u, v);
let n = surface.normal(u, v);
verts.push([p.x, p.y, p.z]);
normals.push([n.x as f32, n.y as f32, n.z as f32]);
}
}
let row = (su + 1) as u32;
for j in 0..sv as u32 {
for i in 0..su as u32 {
let a = base + j * row + i;
let b = a + 1;
let c = a + row;
let d = c + 1;
indices.extend_from_slice(&[a, b, d, a, d, c]);
}
}
}
/// Extract the inclusive `[start, end]` bounds from a truck parameter range.
fn range_bounds(r: (std::ops::Bound<f64>, std::ops::Bound<f64>)) -> (f64, f64) {
use std::ops::Bound::*;
let lo = match r.0 {
Included(v) | Excluded(v) => v,
Unbounded => 0.0,
};
let hi = match r.1 {
Included(v) | Excluded(v) => v,
Unbounded => 1.0,
};
(lo, hi)
}
/// Parse the `nubs` control net + knot vectors out of a `spline-surface`
/// record's token stream into a truck `BSplineSurface`.
fn build_spline_surface(rec: &SatRecord) -> Option<SplineSurf> {
let toks = &rec.tokens;
// Locate the real B-spline block. `nullbs` placeholders (for absent
// rail/path surfaces) precede it; the actual surface is `nubs` (plain xyz
// control points) or `nurbs` (rational — each control point carries a
// weight, so it is stored as xyzw).
let start = toks
.iter()
.position(|t| matches!(t, SatToken::Ident(s) if s == "nubs" || s == "nurbs"))?;
let rational = matches!(&toks[start], SatToken::Ident(s) if s == "nurbs");
let mut p = start + 1;
let deg_u = read_int(toks, &mut p)? as usize;
let deg_v = read_int(toks, &mut p)? as usize;
// Four form flags (closure / singularity in u and v) — skip.
for _ in 0..4 {
read_int(toks, &mut p)?;
}
let n_uknot = read_int(toks, &mut p)? as usize;
let n_vknot = read_int(toks, &mut p)? as usize;
let u_knots = read_knot_vec(toks, &mut p, n_uknot, deg_u)?;
let v_knots = read_knot_vec(toks, &mut p, n_vknot, deg_v)?;
let n_ctrl_u = u_knots.len().checked_sub(deg_u + 1)?;
let n_ctrl_v = v_knots.len().checked_sub(deg_v + 1)?;
if n_ctrl_u == 0 || n_ctrl_v == 0 {
return None;
}
// Control points are stored row-major with u varying fastest (a full row
// of u control points per v step). truck wants `ctrl[i_u][j_v]`.
let total = n_ctrl_u * n_ctrl_v;
let uk = KnotVec::from(u_knots);
let vk = KnotVec::from(v_knots);
if rational {
// Rational: read x, y, z, w and store the homogeneous point (xw, yw,
// zw, w) that truck's `NurbsSurface` expects.
let mut flat: Vec<Vector4> = Vec::with_capacity(total);
for _ in 0..total {
let x = read_float(toks, &mut p)?;
let y = read_float(toks, &mut p)?;
let z = read_float(toks, &mut p)?;
let w = read_float(toks, &mut p)?;
flat.push(Vector4::new(x * w, y * w, z * w, w));
}
let mut ctrl = vec![Vec::with_capacity(n_ctrl_v); n_ctrl_u];
for v in 0..n_ctrl_v {
for u in 0..n_ctrl_u {
ctrl[u].push(flat[v * n_ctrl_u + u]);
}
}
let bs = BSplineSurface::try_new((uk, vk), ctrl).ok()?;
Some(SplineSurf::Nurbs(NurbsSurface::new(bs)))
} else {
let mut flat: Vec<Point3> = Vec::with_capacity(total);
for _ in 0..total {
let x = read_float(toks, &mut p)?;
let y = read_float(toks, &mut p)?;
let z = read_float(toks, &mut p)?;
flat.push(Point3::new(x, y, z));
}
let mut ctrl = vec![Vec::with_capacity(n_ctrl_v); n_ctrl_u];
for v in 0..n_ctrl_v {
for u in 0..n_ctrl_u {
ctrl[u].push(flat[v * n_ctrl_u + u]);
}
}
let bs = BSplineSurface::try_new((uk, vk), ctrl).ok()?;
Some(SplineSurf::Bs(bs))
}
}
/// Read `count` `(knot value, multiplicity)` pairs into an expanded knot
/// vector. ACIS stores the end knots with multiplicity = degree; a clamped
/// B-spline needs degree + 1, so the first and last multiplicities are bumped
/// by one.
fn read_knot_vec(
toks: &[SatToken],
p: &mut usize,
count: usize,
degree: usize,
) -> Option<Vec<f64>> {
let mut knots: Vec<f64> = Vec::new();
for i in 0..count {
let value = read_float(toks, p)?;
let mut mult = read_int(toks, p)? as usize;
if i == 0 || i == count - 1 {
mult += 1;
}
let _ = degree;
for _ in 0..mult {
knots.push(value);
}
}
if knots.len() < 2 {
return None;
}
Some(knots)
}
fn read_int(toks: &[SatToken], p: &mut usize) -> Option<i64> {
while *p < toks.len() {
let t = &toks[*p];
*p += 1;
match t {
SatToken::Integer(v) => return Some(*v),
SatToken::Float(v) => return Some(*v as i64),
// Skip block delimiters / idents that may appear inline.
SatToken::Ident(_) | SatToken::Enum(_) => continue,
_ => return None,
}
}
None
}
fn read_float(toks: &[SatToken], p: &mut usize) -> Option<f64> {
while *p < toks.len() {
let t = &toks[*p];
*p += 1;
match t {
SatToken::Float(v) => return Some(*v),
SatToken::Integer(v) => return Some(*v as f64),
SatToken::Ident(_) | SatToken::Enum(_) => continue,
_ => return None,
}
}
None
}