From f8ceddd303ce8d198fbdf81862631280b82cfaba Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 4 Jul 2026 20:15:17 +0300 Subject: [PATCH] fix(solid): read rational NURBS spline-surface control points with weights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An ACIS spline-surface stored as `nurbs` (rational) carries a weight on each control point, so the control net is a stream of x, y, z, w tuples. The parser read plain x, y, z triples, shifting every control point by one float and scrambling the whole net — the surface, and the solid it bounds, rendered badly deformed. Non-rational `nubs` surfaces were unaffected, which is why only some solids (rational lofts/sweeps with varying weights) broke. Detect `nurbs` vs `nubs`; for rational surfaces read the fourth weight component and build a truck `NurbsSurface` from homogeneous (xw, yw, zw, w) control points so the weights actually shape the surface. Both the truck B-rep path and the bespoke fallback share this parser. Verified on VillaSavoye_MartinHedin2.dwg solid 0x24A1EE (handle 2400750): the tessellated surface now stays within the solid's true extent instead of flying outside it. Co-Authored-By: Claude Opus 4.8 --- src/scene/convert/spline_tess.rs | 106 ++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/src/scene/convert/spline_tess.rs b/src/scene/convert/spline_tess.rs index ba2850432..260a86b5b 100644 --- a/src/scene/convert/spline_tess.rs +++ b/src/scene/convert/spline_tess.rs @@ -8,10 +8,47 @@ // and sample its parametric grid into triangles. use acadrust::entities::acis::{SatDocument, SatFace, SatRecord, SatToken}; -use truck_modeling::{BSplineSurface, KnotVec, ParametricSurface, ParametricSurface3D, Point3}; +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), + Nurbs(NurbsSurface), +} + +impl SplineSurf { + fn parameter_range( + &self, + ) -> ( + (std::ops::Bound, std::ops::Bound), + (std::ops::Bound, std::ops::Bound), + ) { + 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. @@ -26,7 +63,7 @@ pub fn tess_spline_face( let Some(surf_rec) = sat.resolve(face.surface()) else { return; }; - let Some(surface) = build_bspline_surface(surf_rec) else { + let Some(surface) = build_spline_surface(surf_rec) else { return; }; @@ -83,13 +120,16 @@ fn range_bounds(r: (std::ops::Bound, std::ops::Bound)) -> (f64, f64) { /// Parse the `nubs` control net + knot vectors out of a `spline-surface` /// record's token stream into a truck `BSplineSurface`. -fn build_bspline_surface(rec: &SatRecord) -> Option> { +fn build_spline_surface(rec: &SatRecord) -> Option { let toks = &rec.tokens; // Locate the real B-spline block. `nullbs` placeholders (for absent - // rail/path surfaces) precede it; the actual surface is `nubs`. - let start = toks.iter().position(|t| { - matches!(t, SatToken::Ident(s) if s == "nubs" || s == "nurbs") - })?; + // 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; @@ -113,23 +153,45 @@ fn build_bspline_surface(rec: &SatRecord) -> Option> { // 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 mut flat: Vec = 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 uk = KnotVec::from(u_knots); let vk = KnotVec::from(v_knots); - BSplineSurface::try_new((uk, vk), ctrl).ok() + + 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 = 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 = 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