From bd127b8c8d4c8e51a1465e542c253326ba920975 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 29 Oct 2026 09:02:17 -0400 Subject: [PATCH 1/4] claim: SmSurface::ApproximateSurface (#1441) Co-Authored-By: Claude Opus 5.5 (1M context) From 053e49a19b0c3b5319ac134fd79190e6b5b8391e Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 29 Oct 2026 17:48:29 -0400 Subject: [PATCH 2/4] port: SmSurface::ApproximateSurface (surface.surface_approximate) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/units/surface_surface_approximate.rs | 585 ++++++ .../src/units/surface_surface_approximate.rs | 266 +++ crates/solidcore/src/surface/surface.rs | 1 + .../src/surface/surface/approximate.rs | 837 +++++++++ .../surface.surface_approximate/cases.json | 1578 +++++++++++++++++ .../cpp/shim/surface_surface_approximate.cpp | 327 ++++ plan/units/surface.surface_approximate.toml | 12 + 7 files changed, 3606 insertions(+) create mode 100644 crates/difftest/src/units/surface_surface_approximate.rs create mode 100644 crates/solidcore-ffi/src/units/surface_surface_approximate.rs create mode 100644 crates/solidcore/src/surface/surface/approximate.rs create mode 100644 fixtures/surface.surface_approximate/cases.json create mode 100644 harness/cpp/shim/surface_surface_approximate.cpp create mode 100644 plan/units/surface.surface_approximate.toml diff --git a/crates/difftest/src/units/surface_surface_approximate.rs b/crates/difftest/src/units/surface_surface_approximate.rs new file mode 100644 index 0000000000..b0543856d0 --- /dev/null +++ b/crates/difftest/src/units/surface_surface_approximate.rs @@ -0,0 +1,585 @@ +//! Runner for unit `surface.surface_approximate`: `SmSurface::ApproximateSurface` on real +//! B-spline surfaces, planes, cylinders, cones, spheres, tori and surfaces of revolution, +//! bare or inside up to three offset, curve bounded, STEP and volume (rigid transform) +//! surfaces, so every `GetRootSurface` override is reached. +//! +//! C++: `solidcore_ffi::units::surface_surface_approximate` (shim +//! `harness/cpp/shim/surface_surface_approximate.cpp`, which documents the block). +//! Rust: [`solidcore::surface::surface::Surface::approximate_surface_ignoring_status`]. +//! +//! A case is the shim's input block (`tol level just_copy n_wraps wraps.. kind` and the +//! kind's construction data, as `surface.surface_assert_valid`'s); both sides build their +//! own surface from it. The output is the status, the achieved tolerance and, when a +//! surface comes back, its type and its `SmBSplineSurface` part (degrees, control point +//! counts, knots with multiplicities, every control point and weight). Generated cases +//! cover every kind with and without wraps (offsets at positive, negative, zero and +//! nested distances and past a radius of curvature; curve bounded, STEP and volume +//! surfaces, alone and around or inside offsets), copies of B-spline kinds +//! (`bJustCopyBSplines`), starters of degree above 3 (the single-patch starter), simple +//! knots (the patch index check), loose, tight and default tolerances, subdivision +//! levels 0 to 4, at huge and tiny scales. Fixtures: `fixtures/surface.surface_approximate/` +//! (issue #1441's offset quarter cylinder among them). + +// The generator indexes the nets it has just built (as other runners do). +#![allow(clippy::indexing_slicing)] + +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use solidcore::curve::curve::Curve; +use solidcore::error::SolidError; +use solidcore::geom::extent2d::Extent2d; +use solidcore::geom::tol::approx_tol3d; +use solidcore::geom::transform::Transform; +use solidcore::geom::vector2d::Vec2; +use solidcore::geom::vector3d::Vec3; +use solidcore::kernel::Kernel; +use solidcore::nurbs::b_spline_curve::{BSplineCurve, ControlPointForm}; +use solidcore::nurbs::b_spline_surface::{BSplineSurface, SurfParamType}; +use solidcore::nurbs::crv_basic::Curve as NlCurve; +use solidcore::nurbs::math::{CPoint, NOW}; +use solidcore::nurbs::srf_basic::Surface as NlSurface; +use solidcore::surface::cone::Cone; +use solidcore::surface::curve_bounded_surface::CurveBoundedSurface; +use solidcore::surface::offset_surface::OffsetSurface; +use solidcore::surface::plane::Plane; +use solidcore::surface::sphere::{Sphere, SphereOptions}; +use solidcore::surface::srf_in_volume::SrfInVolume; +use solidcore::surface::surf_of_revolution::SurfOfRevolution; +use solidcore::surface::surface::{StepSurface, Surface, SurfaceKind}; +use solidcore::surface::torus::{Torus, TorusOptions}; +use solidcore::surface::volume::{Volume, VolumeKind}; +use solidcore_ffi::units::surface_surface_approximate as ffi; + +use crate::case::CaseContext; +use crate::runner::default_cases; +use crate::{Case, HarnessError, Output, Rng, UnitDiff}; + +/// Generated cases per run, whatever `--seeds` says (it can only raise this). +pub const MIN_GENERATED: usize = 400; + +/// `SM_USE_DEFAULT`. +const USE_DEFAULT: f64 = 9_876_543.21; + +/// `surface.surface_approximate` runner. +pub struct Runner; + +/// The shim's input block. +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Inputs { + block: Vec, +} + +fn herr(e: impl std::fmt::Display) -> HarnessError { + HarnessError::new(e.to_string()) +} + +fn output(flat: Vec) -> Output { + Output::new().values("out", flat) +} + +fn cpp(i: &Inputs) -> Result { + // each case in a fresh child: a native crash fails that case, not the unit run + match solidcore_ffi::isolate::fresh_process(|| ffi::eval(&i.block)) { + Ok(v) => Ok(output(v)), + Err(solidcore_ffi::FfiError::InvalidArgument(r)) => Ok(Output::rejected(r)), + Err(e) => Err(herr(e)), + } +} + +// ---- Rust side ---------------------------------------------------------------------- + +struct Rd<'a> { + v: &'a [f64], + at: usize, +} + +impl Rd<'_> { + fn num(&mut self) -> Result { + let x = *self.v.get(self.at).ok_or_else(|| herr("short block"))?; + self.at += 1; + Ok(x) + } + fn int(&mut self) -> Result { + Ok(self.num()? as usize) + } + fn flag(&mut self) -> Result { + Ok(self.num()? != 0.0) + } + fn vec(&mut self) -> Result { + Ok(Vec3::new(self.num()?, self.num()?, self.num()?)) + } + fn dom(&mut self) -> Result { + Ok(Extent2d::from_bounds( + self.num()?, + self.num()?, + self.num()?, + self.num()?, + )) + } + fn point(&mut self, rational: bool) -> Result { + let (x, y, z, w) = (self.num()?, self.num()?, self.num()?, self.num()?); + Ok(CPoint::new(x, y, z, if rational { w } else { NOW })) + } +} + +/// The surface of `kind` (the builders of `surface.surface_assert_valid`'s runner). +fn build(k: &Kernel, rd: &mut Rd<'_>, kind: usize) -> Result { + if kind == 0 { + let (p, q, nu, nv) = (rd.int()?, rd.int()?, rd.int()?, rd.int()?); + let rational = rd.flag()?; + let ku = (0..=nu + p).map(|_| rd.num()).collect::, _>>()?; + let kv = (0..=nv + q).map(|_| rd.num()).collect::, _>>()?; + let mut net = Vec::new(); + for _ in 0..nu { + net.push((0..nv).map(|_| rd.point(rational)).collect::, _>>()?); + } + let nurb = NlSurface::new(net, p, q, ku, kv).map_err(herr)?; + return Ok(Surface::from(BSplineSurface::from_nurb(nurb, false))); + } + let o = rd.vec()?; + let x = rd.vec()?; + let y = rd.vec()?; + Ok(match kind { + 1 => { + let s = Vec2::new(rd.num()?, rd.num()?); + let d = rd.dom()?; + Surface::from(Plane::new(o, x, y, s, &d)) + } + 2 => { + let (r, s0, s1, h) = (rd.num()?, rd.num()?, rd.num()?, rd.num()?); + let (swap, inside, mngc) = (rd.flag()?, rd.flag()?, rd.flag()?); + Surface::from(Cone::new_cylinder_bounded( + o, x, y, r, s0, s1, h, swap, inside, mngc, + )) + } + 3 => { + let (bot, top, s0, s1, h) = (rd.num()?, rd.num()?, rd.num()?, rd.num()?, rd.num()?); + let (swap, inside, mngc) = (rd.flag()?, rd.flag()?, rd.flag()?); + Surface::from(Cone::new_bounded( + o, x, y, bot, top, s0, s1, h, swap, inside, mngc, + )) + } + 4 => { + let d = rd.dom()?; + let r = rd.num()?; + let (swap_uv, inside_out) = (rd.flag()?, rd.flag()?); + let opts = SphereOptions { + swap_uv, + inside_out, + circle_nurb: None, + }; + Surface::from(Sphere::new(k, o, x, y, d, r, opts).map_err(herr)?) + } + 5 => { + let d = rd.dom()?; + let (major, minor) = (rd.num()?, rd.num()?); + let (swap_uv, inside_out) = (rd.flag()?, rd.flag()?); + let opts = TorusOptions { + swap_uv, + inside_out, + circle_nurb: None, + }; + Surface::from(Torus::new(k, o, x, y, d, major, minor, opts).map_err(herr)?) + } + _ => { + let d = rd.dom()?; + let swap = rd.flag()?; + let (p, n) = (rd.int()?, rd.int()?); + let rational = rd.flag()?; + let pts = (0..n).map(|_| rd.point(rational)).collect::, _>>()?; + let knots = (0..=n + p).map(|_| rd.num()).collect::, _>>()?; + let gen = Curve::from(BSplineCurve::from_nurb( + NlCurve::new(pts, p, knots).map_err(herr)?, + 3, + false, + )); + Surface::from(SurfOfRevolution::new(k, gen, o, x, y, d, swap).map_err(herr)?) + } + }) +} + +/// The `SmStatus` of a Rust error. +fn status(e: &SolidError) -> f64 { + match e { + SolidError::Upstream { code, .. } => f64::from(*code), + _ => -1.0, + } +} + +/// The `SmBSplineSurface` part of a result (`SM_CAST_PTR` of the returned pointer: a +/// copy keeps its kind). +fn b_spline_part<'a>( + k: &Kernel, + s: &'a Surface, +) -> Result, SolidError> { + Ok(match s.kind_with_nurb(k)? { + SurfaceKind::BSpline(b) => Cow::Borrowed(b), + SurfaceKind::Plane(p) => p.base_with_nurb()?, + SurfaceKind::Cone(c) => Cow::Borrowed(c.rev().base()), + SurfaceKind::Sphere(c) => Cow::Borrowed(c.base().base()), + SurfaceKind::Torus(c) => Cow::Borrowed(c.base().base()), + SurfaceKind::SurfOfRevolution(c) => Cow::Borrowed(c.base()), + _ => return Err(SolidError::sm_err("difftest::surface_approximate::b_spline_part")), + }) +} + +/// The shim's `ap_put` record of `s`. +fn put(v: &mut Vec, s: &BSplineSurface) { + let nu = s.number_control_points(SurfParamType::U); + let nv = s.number_control_points(SurfParamType::V); + v.extend([ + s.degree(SurfParamType::U) as f64, + s.degree(SurfParamType::V) as f64, + nu as f64, + nv as f64, + ]); + for dir in [SurfParamType::U, SurfParamType::V] { + match s.knots(dir, None) { + Ok((knots, mults)) => { + v.push(knots.len() as f64); + v.extend(knots); + v.extend(mults.iter().map(|&m| m as f64)); + } + Err(_) => v.push(-1.0), + } + } + for i in 0..nu { + for j in 0..nv { + match s.control_point(ControlPointForm::EuclideanRational, i, j) { + Ok((p, w)) => v.extend([p.x, p.y, p.z, w]), + Err(_) => v.extend([0.0, 0.0, 0.0, -1.0]), + } + } + } +} + +fn rust(i: &Inputs) -> Result { + let k = Kernel::default(); + let mut rd = Rd { + v: &i.block, + at: 0, + }; + let tol = rd.num()?; + let level = rd.int()?; + let just_copy = rd.flag()?; + let n_wrap = rd.int()?; + let mut wraps = Vec::new(); + for _ in 0..n_wrap { + let t = rd.int()?; + let m = match t { + 0 | 1 => 1, + 3 => 12, + _ => 0, + }; + wraps.push((t, (0..m).map(|_| rd.num()).collect::, _>>()?)); + } + let kind = rd.int()?; + let mut surface = build(&k, &mut rd, kind)?; + for (t, d) in wraps { + let at = |i: usize| d.get(i).copied().ok_or_else(|| herr("short wrap")); + surface = match t { + 0 => Surface::from(OffsetSurface::new(&k, at(0)?, surface, true, 0).map_err(herr)?), + 1 => Surface::from( + CurveBoundedSurface::new(surface, Vec::new(), Vec::new(), at(0)? != 0.0) + .map_err(herr)?, + ), + 2 => Surface::from(StepSurface::new(surface, true).map_err(herr)?), + _ => { + let v = |i: usize| -> Result { + Ok(Vec3::new(at(i)?, at(i + 1)?, at(i + 2)?)) + }; + let t = Transform::new(v(0)?, v(3)?, v(6)?, Some(v(9)?)); + let vol = Volume::with_maps(VolumeKind::Transform(t), None, None).map_err(herr)?; + Surface::from(SrfInVolume::new(&k, surface, false, vol, 3, 0).map_err(herr)?) + } + }; + } + let r = match surface.approximate_surface_ignoring_status( + &k, + approx_tol3d(&k, tol), + level, + just_copy, + ) { + Ok(r) => r, + Err(e) => { + return Ok(Output::unported(&e).unwrap_or_else(|| Output::err(e.to_string()))); + } + }; + let mut v = vec![ + r.failure.as_ref().map_or(0.0, status), + r.achieved_tol, + f64::from(u8::from(r.surface.is_some())), + ]; + if let Some(s) = &r.surface { + v.push(s.type_code() as f64); + match b_spline_part(&k, s) { + Ok(b) => put(&mut v, &b), + Err(e) => { + return Ok(Output::unported(&e).unwrap_or_else(|| Output::err(e.to_string()))); + } + } + } + Ok(output(v)) +} + +// ---- generation --------------------------------------------------------------------- + +fn scale(rng: &mut Rng) -> f64 { + match rng.below(8) { + 0 => 10f64.powi(rng.range_i64(-6, -2) as i32), + 1 => 10f64.powi(rng.range_i64(2, 6) as i32), + _ => rng.uniform(0.5, 4.0), + } +} + +/// A unit-ish frame: the identity, a rotation about z or y, or a slightly skewed one. +fn frame(rng: &mut Rng, b: &mut Vec, s: f64) { + let o = [rng.uniform(-2.0, 2.0) * s, rng.uniform(-2.0, 2.0) * s, rng.uniform(-2.0, 2.0) * s]; + let (x, y) = match rng.below(4) { + 0 => ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), + 1 => { + let a = rng.uniform(0.0, 6.0); + ([a.cos(), a.sin(), 0.0], [-a.sin(), a.cos(), 0.0]) + } + 2 => { + let a = rng.uniform(0.0, 6.0); + ([a.cos(), 0.0, a.sin()], [0.0, 1.0, 0.0]) + } + _ => ([1.0, 0.0, 0.0], [rng.uniform(-0.3, 0.3), 1.0, rng.uniform(-0.2, 0.2)]), + }; + b.extend(o); + b.extend(x); + b.extend(y); +} + +fn flag(rng: &mut Rng, p: f64) -> f64 { + f64::from(u8::from(rng.chance(p))) +} + +fn angles(rng: &mut Rng) -> (f64, f64) { + match rng.below(5) { + 0 => (0.0, 360.0), + 1 => (-360.0, 0.0), + 2 => { + let a = rng.uniform(-300.0, 200.0); + (a, a + rng.uniform(1.0, 160.0)) + } + 3 => (0.0, 90.0), + _ => (0.0, rng.uniform(10.0, 359.0)), + } +} + +/// A clamped knot vector for `n` control points of degree `p`: simple interior knots, +/// or interior knots of full multiplicity `p` (piecewise Bezier). +fn clamped(rng: &mut Rng, n: usize, p: usize) -> Vec { + let interior = n - p - 1; + let mut mid: Vec = if interior > 0 && interior.is_multiple_of(p) && rng.chance(0.5) { + let spans = interior / p; + (0..spans) + .flat_map(|i| std::iter::repeat_n((i + 1) as f64 / (spans + 1) as f64, p)) + .collect() + } else { + (0..interior) + .map(|i| (i + 1) as f64 / (interior + 1) as f64) + .collect() + }; + mid.sort_by(f64::total_cmp); + let mut k = vec![0.0; p + 1]; + k.extend(mid); + k.extend(vec![1.0; p + 1]); + k +} + +fn gen_bspline(rng: &mut Rng, b: &mut Vec, s: f64) { + let deg = |rng: &mut Rng| match rng.below(8) { + 0 => 4, + 1 => 5, + _ => 1 + rng.below(3) as usize, + }; + let p = deg(rng); + let q = deg(rng); + let nu = p + 1 + [0, p, 2 * p, 1, 2][rng.below(5) as usize]; + let nv = q + 1 + [0, q, 2 * q, 1, 2][rng.below(5) as usize]; + let rational = rng.chance(0.25); + let ku = clamped(rng, nu, p); + let kv = clamped(rng, nv, q); + let bump = [0.0, 0.3, 1.0][rng.below(3) as usize]; + let pole = rng.chance(0.1); + b.extend([p as f64, q as f64, nu as f64, nv as f64, f64::from(u8::from(rational))]); + b.extend(ku); + b.extend(kv); + for i in 0..nu { + for j in 0..nv { + let u = i as f64 / (nu - 1) as f64; + let v = j as f64 / (nv - 1) as f64; + let w = if rational { rng.uniform(0.5, 2.0) } else { 1.0 }; + let pt = if pole && i == 0 { + // a collapsed boundary row + [s, 0.5 * s, 0.0] + } else { + [(u + 1.0) * s, (v + 0.5) * s, bump * rng.uniform(-1.0, 1.0) * s] + }; + if rational { + b.extend([pt[0] * w, pt[1] * w, pt[2] * w, w]); + } else { + b.extend([pt[0], pt[1], pt[2], 1.0]); + } + } + } +} + +fn gen_case(rng: &mut Rng) -> Inputs { + let kind = rng.below(7) as usize; + let s = scale(rng); + let tol = match rng.below(8) { + 0 => USE_DEFAULT, + 1 => 1e-6 * s, + 2 => 0.0, + 3 => -1e-3 * s, // fabs + 4 => 0.1 * s, + _ => 1e-3 * s, + }; + let level = match rng.below(6) { + 0 => 0, + 1 => 1, + 2 => 4, + _ => 3, + }; + let mut b = vec![tol, f64::from(level), flag(rng, 0.15)]; + let n_wrap = match rng.below(10) { + 0..=2 => 0, + 3 => 2, + 4 => 3, + _ => 1, + }; + b.push(f64::from(n_wrap)); + for _ in 0..n_wrap { + match rng.below(10) { + 0 => b.extend([1.0, flag(rng, 0.5)]), // curve bounded + 1 => b.push(2.0), // STEP + 2 => { + // in a volume: a rigid motion + let a = rng.uniform(0.0, 6.0); + let t = [rng.uniform(-1.0, 1.0) * s, rng.uniform(-1.0, 1.0) * s, 0.5 * s]; + b.push(3.0); + b.extend(t); + b.extend([a.cos(), a.sin(), 0.0, -a.sin(), a.cos(), 0.0, 0.0, 0.0, 1.0]); + } + _ => { + b.push(0.0); + b.push(match rng.below(8) { + 0 => 0.0, + 1 => -rng.uniform(0.05, 0.5) * s, + 2 => rng.uniform(1.0, 3.0) * s, // past a radius of curvature + _ => rng.uniform(0.05, 0.5) * s, + }); + } + } + } + b.push(kind as f64); + if kind == 0 { + gen_bspline(rng, &mut b, s); + return Inputs { block: b }; + } + frame(rng, &mut b, s); + match kind { + 1 => { + let sx = if rng.chance(0.2) { -1.0 } else { rng.uniform(0.2, 3.0) }; + let sy = rng.uniform(0.2, 3.0); + let u0 = rng.uniform(-2.0, 2.0) * s; + let v0 = rng.uniform(-2.0, 2.0) * s; + let du = rng.uniform(0.1, 3.0) * s; + let dv = rng.uniform(0.1, 3.0) * s; + b.extend([sx, sy, u0, v0, u0 + du, v0 + dv]); + } + 2 => { + let (a0, a1) = angles(rng); + b.extend([s * rng.uniform(0.2, 2.0), a0, a1, s * rng.uniform(0.1, 3.0)]); + b.extend([flag(rng, 0.3), flag(rng, 0.3), flag(rng, 0.3)]); + } + 3 => { + let (a0, a1) = angles(rng); + let (bot, top) = match rng.below(4) { + 0 => (0.0, rng.uniform(0.5, 2.0)), + 1 => (rng.uniform(0.5, 2.0), 0.0), + _ => (rng.uniform(0.2, 2.0), rng.uniform(0.2, 2.0)), + }; + b.extend([bot * s, top * s, a0, a1, s * rng.uniform(0.1, 3.0)]); + b.extend([flag(rng, 0.3), flag(rng, 0.3), flag(rng, 0.3)]); + } + 4 => { + let (a0, a1) = angles(rng); + let (v0, v1) = match rng.below(4) { + 0 => (-90.0, 90.0), + 1 => (-90.0, rng.uniform(-80.0, 89.0)), + 2 => (rng.uniform(-89.0, 80.0), 90.0), + _ => { + let v = rng.uniform(-85.0, 60.0); + (v, v + rng.uniform(1.0, 85.0 - v.max(-85.0)).min(89.0 - v)) + } + }; + b.extend([a0, v0, a1, v1, s * rng.uniform(0.2, 3.0)]); + b.extend([flag(rng, 0.3), flag(rng, 0.3)]); + } + 5 => { + let (a0, a1) = angles(rng); + let (v0, v1) = angles(rng); + let major = s * rng.uniform(0.5, 3.0); + let minor = match rng.below(4) { + 0 => major * rng.uniform(1.1, 2.0), // spindle + _ => major * rng.uniform(0.05, 0.9), + }; + b.extend([a0, v0, a1, v1, major, minor]); + b.extend([flag(rng, 0.3), flag(rng, 0.3)]); + } + _ => { + let (a0, a1) = angles(rng); + let p = 1 + rng.below(4) as usize; + let n = p + 1 + rng.below(3) as usize; + let interior = n - p - 1; + let mut knots = vec![0.0; p + 1]; + knots.extend((0..interior).map(|i| (i + 1) as f64 / (interior + 1) as f64)); + knots.extend(vec![1.0; p + 1]); + b.extend([a0, 0.0, a1, 1.0, flag(rng, 0.3)]); + let rational = rng.chance(0.2); + b.extend([p as f64, n as f64, f64::from(u8::from(rational))]); + for i in 0..n { + let t = i as f64 / (n - 1) as f64; + let (r, z) = (s * rng.uniform(0.3, 2.0), s * (t + rng.uniform(0.0, 0.1))); + let w = if rational { rng.uniform(0.5, 2.0) } else { 1.0 }; + b.extend([r * w, 0.0, z * w, w]); + } + b.extend(knots); + } + } + Inputs { block: b } +} + +impl UnitDiff for Runner { + fn id(&self) -> &'static str { + "surface.surface_approximate" + } + + fn generate(&self, rng: &mut Rng) -> Option { + serde_json::to_value(gen_case(rng)).ok() + } + + fn cases(&self, ctx: &CaseContext) -> Result, HarnessError> { + let ctx = CaseContext { + seeds: ctx.seeds.max(MIN_GENERATED), + ..ctx.clone() + }; + default_cases(self, &ctx) + } + + fn run_cpp(&self, case: &Case) -> Result { + cpp(&case.parse()?) + } + + fn run_rust(&self, case: &Case) -> Result { + rust(&case.parse()?) + } +} diff --git a/crates/solidcore-ffi/src/units/surface_surface_approximate.rs b/crates/solidcore-ffi/src/units/surface_surface_approximate.rs new file mode 100644 index 0000000000..0510ca2c5c --- /dev/null +++ b/crates/solidcore-ffi/src/units/surface_surface_approximate.rs @@ -0,0 +1,266 @@ +//! Safe wrapper for unit `surface.surface_approximate`: `SmSurface::ApproximateSurface` +//! on a real B-spline surface, plane, cylinder, cone, sphere, torus or surface of +//! revolution built from plain data, optionally inside up to three offset, curve bounded, +//! STEP or volume (rigid transform) surfaces (block layout in +//! `harness/cpp/shim/surface_surface_approximate.cpp`). + +use crate::{call_with_buffer, FfiError}; + +mod sys { + extern "C" { + pub fn sc_surface_surface_approximate_eval( + input: *const f64, + n: i64, + out: *mut f64, + cap: i64, + len: *mut i64, + ) -> i32; + } +} + +/// Largest subdivision level accepted (each level can double the spans in each +/// direction, and every span is evaluated). +pub const MAX_LEVEL: usize = 4; + +fn bad(what: &str) -> FfiError { + FfiError::InvalidArgument(format!("surface_approximate: {what}")) +} + +struct Rd<'a> { + v: &'a [f64], + at: usize, +} + +impl Rd<'_> { + fn num(&mut self) -> Result { + let x = *self.v.get(self.at).ok_or_else(|| bad("short block"))?; + self.at += 1; + if !x.is_finite() || x.abs() > 1e12 { + return Err(bad("non-finite or huge value")); + } + Ok(x) + } + fn int(&mut self, lo: f64, hi: f64) -> Result { + let x = self.num()?; + if x.fract() != 0.0 || x < lo || x > hi { + return Err(bad("bad count")); + } + Ok(x as usize) + } + fn flag(&mut self) -> Result { + let x = self.num()?; + if x != 0.0 && x != 1.0 { + return Err(bad("bad flag")); + } + Ok(x == 1.0) + } + fn vec(&mut self) -> Result<[f64; 3], FfiError> { + Ok([self.num()?, self.num()?, self.num()?]) + } + fn dom(&mut self) -> Result<[f64; 4], FfiError> { + Ok([self.num()?, self.num()?, self.num()?, self.num()?]) + } +} + +fn len(v: [f64; 3]) -> f64 { + (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt() +} + +/// Two axes long enough and far from parallel (the constructors unitize them). +fn axes(x: [f64; 3], y: [f64; 3]) -> Result<(), FfiError> { + let c = [ + x[1] * y[2] - x[2] * y[1], + x[2] * y[0] - x[0] * y[2], + x[0] * y[1] - x[1] * y[0], + ]; + if len(x) < 1e-6 || len(y) < 1e-6 || len(c) < 1e-3 * len(x) * len(y) { + return Err(bad("degenerate or parallel axes")); + } + Ok(()) +} + +/// An angular interval the analytic constructors accept. +fn angular(d0: f64, d1: f64) -> Result<(), FfiError> { + if d0 >= d1 || d0 < -360.0 || d1 > 360.0 || d1 - d0 > 360.0 { + return Err(bad("bad angular domain")); + } + Ok(()) +} + +/// Non-decreasing knots of positive span, multiplicities at most the order. +fn knots(k: &[f64], p: usize) -> Result<(), FfiError> { + if k.windows(2).any(|w| matches!(w, [a, b] if b < a)) || k.last() <= k.first() { + return Err(bad("bad knots")); + } + let mut run = 1; + for w in k.windows(2) { + run = if matches!(w, [a, b] if a == b) { + run + 1 + } else { + 1 + }; + if run > p + 1 { + return Err(bad("knot multiplicity above the order")); + } + } + Ok(()) +} + +/// `n` control points (`x y z w`), weights positive when rational. +fn points(rd: &mut Rd<'_>, n: usize, rational: bool) -> Result<(), FfiError> { + for _ in 0..n { + rd.vec()?; + let w = rd.num()?; + if rational && w <= 1e-6 { + return Err(bad("weight not positive")); + } + } + Ok(()) +} + +fn check(block: &[f64]) -> Result<(), FfiError> { + let mut rd = Rd { v: block, at: 0 }; + rd.num()?; // tol + rd.int(0.0, MAX_LEVEL as f64)?; + rd.flag()?; + let n_wrap = rd.int(0.0, 3.0)?; + for _ in 0..n_wrap { + match rd.int(0.0, 3.0)? { + 0 => { + rd.num()?; + } + 1 => { + rd.flag()?; + } + 2 => {} + _ => { + rd.vec()?; + let x = rd.vec()?; + let y = rd.vec()?; + let z = rd.vec()?; + axes(x, y)?; + axes(y, z)?; + axes(z, x)?; + } + } + } + let kind = rd.int(0.0, 6.0)?; + if kind == 0 { + let p = rd.int(1.0, 5.0)?; + let q = rd.int(1.0, 5.0)?; + let nu = rd.int(2.0, 32.0)?; + let nv = rd.int(2.0, 32.0)?; + if nu <= p || nv <= q { + return Err(bad("too few control points for the degree")); + } + let rational = rd.flag()?; + let ku: Vec = (0..=nu + p).map(|_| rd.num()).collect::>()?; + let kv: Vec = (0..=nv + q).map(|_| rd.num()).collect::>()?; + knots(&ku, p)?; + knots(&kv, q)?; + points(&mut rd, nu * nv, rational)?; + } else { + rd.vec()?; + let x = rd.vec()?; + let y = rd.vec()?; + axes(x, y)?; + match kind { + 1 => { + let sx = rd.num()?; + let sy = rd.num()?; + let d = rd.dom()?; + if sx.abs() < 1e-6 || sy.abs() < 1e-6 || d[0] >= d[2] || d[1] >= d[3] { + return Err(bad("degenerate plane scale or domain")); + } + } + 2 | 3 => { + let (r0, r1) = if kind == 2 { + let r = rd.num()?; + (r, r) + } else { + (rd.num()?, rd.num()?) + }; + let s0 = rd.num()?; + let s1 = rd.num()?; + let h = rd.num()?; + angular(s0, s1)?; + if h <= 1e-6 || r0.abs().max(r1.abs()) < 1e-6 || (kind == 2 && r0 <= 1e-6) { + return Err(bad("degenerate height or radius")); + } + rd.flag()?; + rd.flag()?; + rd.flag()?; + } + 4 => { + let d = rd.dom()?; + let r = rd.num()?; + angular(d[0], d[2])?; + if d[1] >= d[3] || d[1] < -90.0 || d[3] > 90.0 || r <= 1e-6 { + return Err(bad("sphere domain or radius")); + } + rd.flag()?; + rd.flag()?; + } + 5 => { + let d = rd.dom()?; + let major = rd.num()?; + let minor = rd.num()?; + angular(d[0], d[2])?; + angular(d[1], d[3])?; + if major <= 1e-6 || minor <= 1e-6 { + return Err(bad("torus radii")); + } + rd.flag()?; + rd.flag()?; + } + _ => { + let d = rd.dom()?; + angular(d[0], d[2])?; + if d[1] >= d[3] { + return Err(bad("revolution domain")); + } + rd.flag()?; + let p = rd.int(1.0, 5.0)?; + let n = rd.int(2.0, 32.0)?; + if n <= p { + return Err(bad("too few generator points")); + } + let rational = rd.flag()?; + points(&mut rd, n, rational)?; + let k: Vec = (0..=n + p).map(|_| rd.num()).collect::>()?; + knots(&k, p)?; + } + } + } + if rd.at != block.len() { + return Err(bad("trailing values")); + } + Ok(()) +} + +/// Runs `SmSurface::ApproximateSurface(ctx, tol, achieved, level, result, just_copy)` on +/// the surface `block` describes and returns the shim's output record. +/// +/// # Errors +/// +/// [`FfiError::InvalidArgument`] for a block the constructors cannot take safely +/// (non-finite or huge values, degenerate axes, radii, heights, scales or domains, +/// angular domains outside the constructors' ranges, bad knots, non-positive weights, +/// degenerate transform axes, more than three wraps, a level above [`MAX_LEVEL`]); the +/// shim's status otherwise. +pub fn eval(block: &[f64]) -> Result, FfiError> { + check(block)?; + call_with_buffer(1 << 16, |out, len| { + // SAFETY: the validated block outlives the call; output and length pointers come + // from call_with_buffer. + unsafe { + sys::sc_surface_surface_approximate_eval( + block.as_ptr(), + block.len() as i64, + out.as_mut_ptr(), + out.len() as i64, + &raw mut *len, + ) + } + }) +} diff --git a/crates/solidcore/src/surface/surface.rs b/crates/solidcore/src/surface/surface.rs index 79948dab90..6c6d73717c 100644 --- a/crates/solidcore/src/surface/surface.rs +++ b/crates/solidcore/src/surface/surface.rs @@ -7,6 +7,7 @@ //! each lives in its own submodule and adds methods to [`Surface`] (or free //! functions). +pub mod approximate; pub mod assert_valid; pub mod p1; pub mod p10; diff --git a/crates/solidcore/src/surface/surface/approximate.rs b/crates/solidcore/src/surface/surface/approximate.rs new file mode 100644 index 0000000000..ffd6e770ad --- /dev/null +++ b/crates/solidcore/src/surface/surface/approximate.rs @@ -0,0 +1,837 @@ +//! Approximation of any surface by a B-spline surface within a tolerance +//! (`ApproximateSurface`): piecewise cubic Bezier patches from the surface's corner +//! data, split until every patch is close enough, then knots removed again. +//! +//! Upstream: `source/SMLib/src/SmSurface.cpp`, `source/SMLib/inc/SmSurface.h`, +//! `source/SMLib/src/SmBSplineSurfaceNLib.cpp`, `source/SMLib/inc/SmBSplineSurface.h`, +//! `source/SMLib/inc/SmCubicBezierSurface.h`, `source/SMLib/inc/SmOffsetSurface.h`, +//! `source/SMLib/inc/SmCurveBoundedSurface.h`, `source/SMLib/inc/SmSTEPSurface.h`, +//! `source/SMLib/inc/SmSrfInVolume.h`. +//! +//! | Rust | Upstream | +//! |-----------------------------------------|----------------------------------------------| +//! | [`Surface::approximate_surface`], [`ApproximatedSurface`] | `SmSurface::ApproximateSurface` | +//! | [`Surface::approximate_surface_ignoring_status`] | the same, for a caller that ignores its status | +//! | [`Surface::compute_cubic_bezier_approx`] | `SmSurface::ComputeCubicBezierApprox` | +//! | [`CubicBezierSurface`] | `SmCubicBezierSurface` (its points only) | +//! | `root_surface` (private) | `GetRootSurface` (virtual) | +//! | [`BSplineSurface::degree_elevate`] | `SmBSplineSurface::DegreeElevate` | +//! | [`BSplineSurface::insert_knots`] | `SmBSplineSurface::InsertKnots` (NLib form) | +//! | [`BSplineSurface::remove_knots`] | `SmBSplineSurface::RemoveKnots` | +//! +//! None of these is in `harness/callgraph/units.json`: they are listed in +//! `harness/callgraph/unused_functions.txt` (no upstream test or `SM_API` entry point +//! reaches them; `SmBrepData`'s `bWriteAsBSplines` writer and the graphics output call +//! `ApproximateSurface`). +//! +//! Deviations: +//! - **Outputs.** `rpNewBSplineSurface` and `dAchievedTol` are the fields of +//! [`ApproximatedSurface`]. The new surface is a [`Surface`]: with +//! `bJustCopyBSplines` the copy of a plane, cone, sphere, torus or swept surface is +//! that kind (upstream returns it through an `SmBSplineSurface*`); otherwise it is +//! always a B-spline. [`Surface::approximate_surface`] returns the status as `Err` +//! (the achieved tolerance is then lost); a caller that ignores the status, as every +//! upstream caller does, uses [`Surface::approximate_surface_ignoring_status`]. +//! - **Patch index check.** When a starter surface's knots are not all of full +//! multiplicity, a patch's control points lie outside the net; upstream signals +//! `SM_ERR` but returns `SM_SUCCESS` without surface, as the port does (`Ok` with +//! `surface: None`). +//! - **Root surface.** `GetRootSurface` of an offset, curve bounded surface or surface +//! in a volume follows its base; the chain is walked in a loop bounded by +//! [`MAX_GEOMETRY_NESTING`] (`InvalidInput` beyond). The root of a plane, cone, +//! sphere, torus or swept surface is its `SmBSplineSurface` part, its NURB made as +//! `GetDegree` and the copy constructor make it (`GetOrCreateGwNurbPointer`). +//! - **Degree elevation below the degree.** [`BSplineSurface::degree_elevate`] returns +//! `InvalidInput` for a new degree below the current one, where upstream passes the +//! negative increment on to `N_SrfElevateDegree` (`ApproximateSurface` only elevates +//! degrees below 3, so it never asks for that). +//! - **Subdivision rounds.** `2 * lSubdivisionLevel + 1` wraps around in upstream's +//! `ULONG`; the port returns `InvalidInput` when it overflows `usize`. +//! - **Starter construction.** A failed `CreateCanonical` of the single-patch starter +//! (a degenerate natural domain) is returned; upstream dereferences the `NULL` +//! surface it leaves. +//! - **Ignored statuses.** As upstream: `Copy`, `GetKnots`, `GetControlPoint`, +//! `SetControlPoint`, `DegreeElevate` and `RemoveKnots` (a failed elevation or +//! removal leaves the surface unchanged); an unported dependency still stops the +//! port. +//! - **Memory and contexts.** `SmContext`, `SmNLibStackHandler` and `NL_STACKS` are +//! dropped; the debug dumps and drawing (`SM_DEBUG_CODE`) are omitted. + +use std::borrow::Cow; + +use crate::error::{ignored, Result, SolidError}; +use crate::geom::extent1d::Extent1d; +use crate::geom::extent2d::Extent2d; +use crate::geom::vector2d::Point2; +use crate::geom::vector3d::{Point3, Vec3}; +use crate::kernel::Kernel; +use crate::nurbs::b_spline_curve::{ControlPointForm, KnotType}; +use crate::nurbs::b_spline_surface::{ + BSplineSurface, BSplineSurfaceForm, CanonicalSurfaceData, SurfParamType, +}; +use crate::nurbs::b_spline_surface_n_lib::p2::EvaluateFlags; +use crate::nurbs::knots::Side; +use crate::nurbs::srf_basic::Direction; +use crate::nurbs::srf_tool::p1::insert_knots; +use crate::nurbs::srf_tool::p2::elevate_degree; +use crate::nurbs::srf_tool::p3::remove_all_knots; +use crate::nurbs::srf_tool::p4::remove_knots_constrained; + +use super::p2::{Surface, SurfaceKind, B_SPLINE_SURFACE_TYPE, MAX_GEOMETRY_NESTING}; + +const OP_APPROX: &str = "surface::surface::approximate::approximate_surface"; +const OP_BEZIER: &str = "surface::surface::approximate::compute_cubic_bezier_approx"; +const OP_ROOT: &str = "surface::surface::approximate::root_surface"; +const OP_ELEVATE: &str = "surface::surface::approximate::degree_elevate"; +const OP_INSERT: &str = "surface::surface::approximate::insert_knots"; +const OP_REMOVE: &str = "surface::surface::approximate::remove_knots"; + +/// A bicubic Bezier patch (`SmCubicBezierSurface`): its 4x4 control points, +/// `points[v][u]` (`m_vP[v][u]`). +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CubicBezierSurface { + /// `m_vP`: row `v`, column `u`. + pub points: [[Point3; 4]; 4], +} + +impl Default for CubicBezierSurface { + fn default() -> Self { + Self { + points: [[Point3::ZERO; 4]; 4], + } + } +} + +/// The result of [`Surface::approximate_surface`]. +#[derive(Debug, Clone)] +pub struct ApproximatedSurface { + /// `rpNewBSplineSurface`: the approximating surface with the fewest subdivisions + /// within the tolerance; `None` when the finest allowed one is still out of it + /// (and in the cases the module deviations list). + pub surface: Option, + /// `dAchievedTol`: the largest sampled distance of the last test (`0` for a copied + /// B-spline, `-1` when nothing was tested). + pub achieved_tol: f64, + /// The status upstream returns and its caller ignores + /// ([`Surface::approximate_surface_ignoring_status`]); always `None` from + /// [`Surface::approximate_surface`], which returns it as `Err`. + pub failure: Option, +} + +impl Surface { + /// A B-spline surface within `approx_tol_3d` (an `SmApproxTol3d` value: + /// [`crate::geom::tol::approx_tol3d`] converts a caller's `double`) of this one + /// (`ApproximateSurface`), + /// splitting the spans up to `subdivision_level` times in each direction. With + /// `just_copy_b_splines` a surface that is an `SmBSplineSurface` (a B-spline, + /// plane, cone, sphere, torus or swept surface) is copied instead. + /// + /// # Errors + /// + /// `SM_ERR` when no subdivision level is within the tolerance, or the starter + /// surface's degree is above 3; those of the evaluations, of the starter surface's + /// construction and of knot insertion; `InvalidInput` for a root surface chain + /// deeper than [`MAX_GEOMETRY_NESTING`]. + pub fn approximate_surface( + &self, + k: &Kernel, + approx_tol_3d: f64, + subdivision_level: usize, + just_copy_b_splines: bool, + ) -> Result { + let mut achieved_tol = -1.0; + let surface = self.approximate_surface_impl( + k, + approx_tol_3d, + &mut achieved_tol, + subdivision_level, + just_copy_b_splines, + )?; + Ok(ApproximatedSurface { + surface, + achieved_tol, + failure: None, + }) + } + + /// [`Surface::approximate_surface`] for a caller that ignores its status, as + /// `SmBrepData`'s B-spline writer does: the outputs as the call leaves them (no + /// surface on failure; the achieved tolerance of the last test) and the failed + /// status in [`ApproximatedSurface::failure`]. + /// + /// # Errors + /// + /// [`SolidError::Unported`] and [`SolidError::PreparationRequired`] only. + pub fn approximate_surface_ignoring_status( + &self, + k: &Kernel, + approx_tol_3d: f64, + subdivision_level: usize, + just_copy_b_splines: bool, + ) -> Result { + let mut achieved_tol = -1.0; + let (surface, failure) = match self.approximate_surface_impl( + k, + approx_tol_3d, + &mut achieved_tol, + subdivision_level, + just_copy_b_splines, + ) { + Ok(surface) => (surface, None), + Err(e) if e.is_unported() || matches!(e, SolidError::PreparationRequired { .. }) => { + return Err(e) + } + Err(e) => (None, Some(e)), + }; + Ok(ApproximatedSurface { + surface, + achieved_tol, + failure, + }) + } + + /// The body of `ApproximateSurface`, writing `dAchievedTol` as it goes. + fn approximate_surface_impl( + &self, + k: &Kernel, + approx_tol_3d: f64, + achieved_tol: &mut f64, + subdivision_level: usize, + just_copy_b_splines: bool, + ) -> Result> { + const OP: &str = OP_APPROX; + + // init output + *achieved_tol = -1.0; + + // low work - when asked, just copy BSplines + if just_copy_b_splines && self.is_kind_of(B_SPLINE_SURFACE_TYPE)? { + let surface = ignored(self.copy(k))?; + *achieved_tol = 0.0; + return Ok(surface); + } + + // arrive here when working with any surface not derived from SmBSplineSurface + + // get RootSurface - not every surface type has one, and make the starter + // output surface + let mut approx_surf: Option = None; + if let Some(root) = self.root_surface(k)? { + // let starter ApproxSurf equal a copy of the SmBSplineSurface part of the + // root surface, unless it has degree > 3 in either direction: our + // algorithm requires cubic. + let u_deg = root.degree(SurfParamType::U); + let v_deg = root.degree(SurfParamType::V); + if u_deg <= 3 && v_deg <= 3 { + approx_surf = Some(root.copy()); + } + } + + // If not a BSplineSurface of degree 3 or less: + let mut approx_surf = if let Some(s) = approx_surf { + s + } else { + // let starter ApproxSurf equal a degree 3x3 single patch with the same + // domain as the RootSurface + let uv_domain = self.natural_uv_domain_with_nurb(k)?; + let mut control_points = [Point3::ZERO; 16]; + for (ii, row) in control_points.chunks_exact_mut(4).enumerate() { + for (jj, p) in row.iter_mut().enumerate() { + *p = Point3::new(ii as f64, jj as f64, 1.0); + } + } + let mults = [4, 4]; + let u_starter_knots = [uv_domain.u_min(), uv_domain.u_max()]; + let v_starter_knots = [uv_domain.v_min(), uv_domain.v_max()]; + BSplineSurface::create_canonical(&CanonicalSurfaceData { + u_degree: 3, + v_degree: 3, + control_points: &control_points, + form: BSplineSurfaceForm::Unspecified, + u_multiplicities: &mults, + v_multiplicities: &mults, + u_knots: &u_starter_knots, + v_knots: &v_starter_knots, + knot_type: KnotType::UniformKnots, + weights: None, + uv_domain: None, + })? + }; + + // specify the control point form + let form = if approx_surf.is_rational() { + ControlPointForm::EuclideanRational + } else { + ControlPointForm::NonRational + }; + + // Nurb locals: upstream's arrays live across the iterations, so a failed + // `GetKnots` (no NURB) leaves the previous round's knots + let mut u_knots: Vec = Vec::new(); + let mut v_knots: Vec = Vec::new(); + let mut more_knots: Vec = Vec::new(); + + let mut testing_u_dir = false; // We'll toggle this each big iteration. + + // Process u- and v-direction for each subD level, so iterate to 2x SubD level. + let last_sub_d = subdivision_level + .checked_mul(2) + .and_then(|n| n.checked_add(1)) + .ok_or_else(|| SolidError::invalid_input(OP, "subdivision level too large"))?; + for _sub_d in 0..=last_sub_d { + testing_u_dir = !testing_u_dir; // Toggle + + let mut u_deg = approx_surf.degree(SurfParamType::U); + let mut v_deg = approx_surf.degree(SurfParamType::V); + + // no work - high degree surfaces + if u_deg > 3 || v_deg > 3 { + return Err(SolidError::sm_err(OP)); + } + + // elevate surface degrees to 3 (statuses ignored) + if u_deg < 3 { + ignored(approx_surf.degree_elevate(SurfParamType::U, 3))?; + } + if v_deg < 3 { + ignored(approx_surf.degree_elevate(SurfParamType::V, 3))?; + } + + u_deg = approx_surf.degree(SurfParamType::U); + v_deg = approx_surf.degree(SurfParamType::V); + + // Update pApproxSurf using cubic Bezier approximations to each patch. + + // get knots of the subdivided surface (statuses ignored: without NURB the + // arrays keep their values) + if let Some((knots, _mult)) = ignored(approx_surf.knots(SurfParamType::U, None))? { + u_knots = knots; + } + if let Some((knots, _mult)) = ignored(approx_surf.knots(SurfParamType::V, None))? { + v_knots = knots; + } + + // For each surface patch, compute the cubic Bezier approximation from the + // actual surface and copy those control Points into new approx surface. + for (iu, u_span) in u_knots.windows(2).enumerate() { + for (iv, v_span) in v_knots.windows(2).enumerate() { + let (&[u0, u1], &[v0, v1]) = (u_span, v_span) else { + continue; + }; + let dom = Extent2d::from_points(Point2::new(u0, v0), Point2::new(u1, v1)); + + // build cubic bezier approximation to this curve + let cb = self.compute_cubic_bezier_approx(k, &dom)?; + let u_min = iu * 3; + let v_min = iv * 3; + let num_ctrl_u = approx_surf.number_control_points(SurfParamType::U); + let num_ctrl_v = approx_surf.number_control_points(SurfParamType::V); + + // copy span approximation control points into new approx surface + for iub in 0..=3 { + for ivb in 0..=3 { + if u_min + iub >= num_ctrl_u || v_min + ivb >= num_ctrl_v { + // SE(SM_ERR): some problem here with knots; upstream + // returns SM_SUCCESS without surface + return Ok(None); + } + // (the point read is overwritten; statuses ignored) + let _ = + ignored(approx_surf.control_point(form, u_min + iub, v_min + ivb))?; + let weight = 1.0; + let p = cb + .points + .get(ivb) + .and_then(|row| row.get(iub)) + .copied() + .ok_or_else(|| SolidError::sm_err(OP))?; + ignored(approx_surf.set_control_point( + form, + u_min + iub, + v_min + ivb, + p, + weight, + ))?; + } + } + } // end iter every V patch + } // end iter every U patch + + // Test the accuracy of this control polygon approx surface. + // If good enough then we can stop here. + + // We'll test points at 1/4, 2/4, 3/4: + let num_between: usize = 3; + let fraction = (num_between + 1) as f64; + let mut failed = false; + + if let Some((knots, _)) = ignored(approx_surf.knots(SurfParamType::U, None))? { + u_knots = knots; + } + if let Some((knots, _)) = ignored(approx_surf.knots(SurfParamType::V, None))? { + v_knots = knots; + } + + let mut ivl_inner = Extent1d::new(); + let mut ivl_outer = Extent1d::new(); + + // init max value + *achieved_tol = -1.0; + + more_knots.clear(); + + let (outer_knots, inner_knots) = if testing_u_dir { + (&u_knots, &v_knots) + } else { + (&v_knots, &u_knots) + }; + + // for every surface patch + for outer_span in outer_knots.windows(2) { + failed = false; // Starting over on a new row. + let &[o0, o1] = outer_span else { + continue; + }; + // (knots ascend: the status is ignored, as upstream) + let _ = ivl_outer.set_min_max(o0, o1); + + // for every 'outer' dir sample point + for jj in 1..=num_between { + let outer_param = ivl_outer.evaluate(jj as f64 / fraction); + + // for every 'inner' dir patch + for inner_span in inner_knots.windows(2) { + let &[i0, i1] = inner_span else { + continue; + }; + let _ = ivl_inner.set_min_max(i0, i1); + + // for every 'inner' dir sample point + for kk in 1..=num_between { + let inner_param = ivl_inner.evaluate(kk as f64 / fraction); + + let uv = if testing_u_dir { + Point2::new(outer_param, inner_param) + } else { + Point2::new(inner_param, outer_param) + }; + + // Eval points on both surfaces and check distance. + let pnt = self.evaluate_point(k, uv)?; + let approx_pnt = approx_surf.evaluate_point(uv)?; + + let dist = pnt.distance(approx_pnt); + + // save max achieved tol + if dist > *achieved_tol { + *achieved_tol = dist; + } + + // check gap for failure + if dist > approx_tol_3d { + // mark the approx surface as no good and quit + failed = true; + + // Add a U or V knot between existing knots, multiplicity + // lUDeg or lVDeg, into the 'outer' direction knots. + let deg = if testing_u_dir { u_deg } else { v_deg }; + for _ in 0..deg { + more_knots.push((o0 + o1) / 2.0); + } + + break; // out of 'inner' dir samples into 'inner' dir patches + } + } // end iter every 'inner' dir sample point + + if failed { + break; // out of 'inner' dir patches into 'outer' dir samples + } + } // end iter every 'inner' dir patch + + if failed { + break; // out of 'outer' dir samples into 'outer' dir patches + } + } // end iter every 'outer' patch sample point + + // ... continue on to the next 'outer' dir row/column of patches + } // end iter every 'outer' dir patch + + // when the approx surface is good enough (upstream tests the flag of the + // last row or column only) + if !failed { + // Remove excess knots: the result does not have to be piecewise Bezier. + ignored(approx_surf.remove_knots(approx_tol_3d, true, true, None, None))?; + + // all done + return Ok(Some(Surface::from(approx_surf))); + } + + // arrive here when current subdivision level is not within tolerance - + // subdivide again + if more_knots.is_empty() { + // Can't happen? Infinite loop if it did. + return Err(SolidError::sm_err(OP)); + } + let param = if testing_u_dir { + SurfParamType::U + } else { + SurfParamType::V + }; + approx_surf.insert_knots(param, &more_knots)?; + } // end iter lSubD + + // arrive here when approximation failed to converge + Err(SolidError::sm_err(OP)) + } + + /// The bicubic Bezier patch over `uv_domain` from this surface's positions, first + /// derivatives and twists at the domain's corners, scaled to the domain + /// (`ComputeCubicBezierApprox`). + /// + /// # Errors + /// + /// Those of the evaluations. + pub fn compute_cubic_bezier_approx( + &self, + k: &Kernel, + uv_domain: &Extent2d, + ) -> Result { + const OP: &str = OP_BEZIER; + // exact tangent values + let flags = EvaluateFlags { + u_side: Side::Left, + v_side: Side::Left, + only_upper_half: false, + non_zero_tangents: false, + }; + let size = uv_domain.size(); + let corner = |uv: Point2| -> Result<[[Vec3; 2]; 2]> { + let m = self.evaluate(k, uv, 1, 1, flags)?; + let at = |i: usize, j: usize| -> Result { + m.get(i) + .and_then(|row| row.get(j)) + .copied() + .ok_or_else(|| SolidError::sm_err(OP)) + }; + Ok([[at(0, 0)?, at(0, 1)?], [at(1, 0)?, at(1, 1)?]]) + }; + let mut cb = CubicBezierSurface::default(); + let p = &mut cb.points; + + // Now load control polygon at corners using hermite information. + { + let mat = corner(uv_domain.min())?; + let p00 = mat[0][0]; + let du = mat[1][0] * size.x; + let dv = mat[0][1] * size.y; + let duv = mat[1][1] * size.x * size.y; + let p01 = p00 + du / 3.0; + let p10 = p00 + dv / 3.0; + let p11 = p10 + p01 - p00 + duv / 9.0; + p[0][0] = p00; + p[0][1] = p01; + p[1][0] = p10; + p[1][1] = p11; + } + + { + let mat = corner(uv_domain.max())?; + let p00 = mat[0][0]; + let du = mat[1][0] * size.x; + let dv = mat[0][1] * size.y; + let duv = mat[1][1] * size.x * size.y; + let p01 = p00 - du / 3.0; + let p10 = p00 - dv / 3.0; + let p11 = p10 + p01 - p00 + duv / 9.0; + p[3][3] = p00; + p[3][2] = p01; + p[2][3] = p10; + p[2][2] = p11; + } + + { + let mat = corner(uv_domain.evaluate(1.0, 0.0))?; + let p00 = mat[0][0]; + let du = mat[1][0] * size.x; + let dv = mat[0][1] * size.y; + let duv = mat[1][1] * size.x * size.y; + let p01 = p00 + dv / 3.0; + let p10 = p00 - du / 3.0; + let p11 = p10 + p01 - p00 - duv / 9.0; + p[0][3] = p00; + p[0][2] = p10; + p[1][3] = p01; + p[1][2] = p11; + } + + { + let mat = corner(uv_domain.evaluate(0.0, 1.0))?; + let p00 = mat[0][0]; + let du = mat[1][0] * size.x; + let dv = mat[0][1] * size.y; + let duv = mat[1][1] * size.x * size.y; + let p10 = p00 + du / 3.0; + let p01 = p00 - dv / 3.0; + let p11 = p10 + p01 - p00 - duv / 9.0; + p[3][0] = p00; + p[2][0] = p01; + p[3][1] = p10; + p[2][1] = p11; + } + + Ok(cb) + } + + /// `GetRootSurface`: the `SmBSplineSurface` part of this surface (a B-spline, plane, + /// cone, sphere, torus or swept surface, its NURB made when missing), followed + /// through offset, curve bounded and volume surfaces to their base; `None` for a + /// STEP surface. + /// + /// # Errors + /// + /// `InvalidInput` for a chain deeper than [`MAX_GEOMETRY_NESTING`]; + /// [`SolidError::Unported`] from a NURB's make. + fn root_surface(&self, k: &Kernel) -> Result>> { + let mut cur = self; + for _ in 0..=MAX_GEOMETRY_NESTING { + let next = match cur.kind() { + SurfaceKind::OffsetSurface(s) => s.base_surface(), + SurfaceKind::CurveBounded(s) => s.basis(), + SurfaceKind::SrfInVolume(s) => s.surface(), + SurfaceKind::Step(_) => return Ok(None), + SurfaceKind::BSpline(s) => return Ok(Some(Cow::Borrowed(s))), + SurfaceKind::Plane(s) => return Ok(Some(s.base_for_query())), + SurfaceKind::SurfOfExtrusion(_) => { + // a failed make leaves the surface without NURB (status ignored) + return match cur.extrusion_base_with_nurb(k) { + Ok(base) => Ok(Some(base)), + Err(e) if e.is_unported() => Err(e), + Err(_) => match cur.kind() { + SurfaceKind::SurfOfExtrusion(s) => Ok(Some(Cow::Borrowed(s.base()))), + _ => Ok(None), + }, + }; + } + SurfaceKind::Cone(_) + | SurfaceKind::Sphere(_) + | SurfaceKind::Torus(_) + | SurfaceKind::SurfOfRevolution(_) => { + let base = match cur.kind_with_nurb(k)? { + SurfaceKind::Cone(s) => s.rev().base(), + SurfaceKind::Sphere(s) => s.base().base(), + SurfaceKind::Torus(s) => s.base().base(), + SurfaceKind::SurfOfRevolution(s) => s.base(), + _ => return Err(SolidError::sm_err(OP_ROOT)), + }; + return Ok(Some(Cow::Borrowed(base))); + } + }; + cur = next; + } + Err(SolidError::invalid_input( + OP_ROOT, + "root surface chain deeper than MAX_GEOMETRY_NESTING", + )) + } +} + +impl BSplineSurface { + /// Raises the degree in `param`'s direction to `new_degree` (`DegreeElevate`). + /// + /// # Errors + /// + /// `NULL` pointer without NURB; `InvalidInput` for a new degree below the + /// current one; those of the NLib elevation (the surface is then unchanged). + pub fn degree_elevate(&mut self, param: SurfParamType, new_degree: usize) -> Result<()> { + const OP: &str = OP_ELEVATE; + let dir = if param == SurfParamType::V { + Direction::V + } else { + Direction::U + }; + let increm = new_degree + .checked_sub(self.degree(param)) + .ok_or_else(|| SolidError::invalid_input(OP, "new degree below the degree"))?; + let sur_p = self + .nurb + .as_ref() + .ok_or_else(|| SolidError::null_pointer(OP))?; + let sur = elevate_degree(sur_p, increm, dir).map_err(SolidError::nl_ser(OP))?; + + // place output sur in a temporary and swap the NURBs + self.nurb = Self::from_gw_nurb(&sur).nurb; + Ok(()) + } + + /// Refines the knot vector in `param`'s direction with `new_knots`, an ascending + /// list (`InsertKnots`, the `SmTArray&` form). + /// + /// # Errors + /// + /// `NULL` pointer without NURB; those of the NLib insertion (the surface is then + /// unchanged). + pub fn insert_knots(&mut self, param: SurfParamType, new_knots: &[f64]) -> Result<()> { + const OP: &str = OP_INSERT; + let dir = if param == SurfParamType::U { + Direction::U + } else { + Direction::V + }; + let sur_p = self + .nurb + .as_ref() + .ok_or_else(|| SolidError::null_pointer(OP))?; + + // Make sur with the union of surP target knot vector and the new knots + let sur = insert_knots(sur_p, new_knots, dir).map_err(SolidError::nl_ser(OP))?; + + // build a surface from it and swap the NURBs + self.nurb = Self::from_gw_nurb(&sur).nurb; + Ok(()) + } + + /// Removes the knots whose removal keeps the surface within `approx_tol_3d`, in u + /// and/or v (both when neither is asked for), except the knots in `kept_u_knots` / + /// `kept_v_knots` (`RemoveKnots`). + /// + /// # Errors + /// + /// `NULL` pointer without NURB; those of the NLib removal (the surface is then + /// unchanged). + pub fn remove_knots( + &mut self, + approx_tol_3d: f64, + remove_u_knots: bool, + remove_v_knots: bool, + kept_u_knots: Option<&[f64]>, + kept_v_knots: Option<&[f64]>, + ) -> Result<()> { + const OP: &str = OP_REMOVE; + let dir = if remove_u_knots && remove_v_knots { + Direction::UV + } else if remove_u_knots { + Direction::U + } else if remove_v_knots { + Direction::V + } else { + Direction::UV + }; + let sur_p = self + .nurb + .as_ref() + .ok_or_else(|| SolidError::null_pointer(OP))?; + let sur = if kept_u_knots.is_some() || kept_v_knots.is_some() { + remove_knots_constrained(sur_p, kept_u_knots, kept_v_knots, approx_tol_3d, dir) + } else { + remove_all_knots(sur_p, approx_tol_3d, dir) + } + .map_err(SolidError::nl_ser(OP))?; + + self.nurb = Self::from_gw_nurb(&sur).nurb; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::surface::cone::Cone; + use crate::surface::offset_surface::OffsetSurface; + + fn kernel() -> Kernel { + Kernel::default() + } + + fn bilinear() -> BSplineSurface { + let pts = [ + Point3::new(0.0, 0.0, 0.0), + Point3::new(0.0, 1.0, 0.2), + Point3::new(1.0, 0.0, 0.1), + Point3::new(1.0, 1.0, 0.0), + ]; + BSplineSurface::create_canonical(&CanonicalSurfaceData { + u_degree: 1, + v_degree: 1, + control_points: &pts, + form: BSplineSurfaceForm::Unspecified, + u_multiplicities: &[2, 2], + v_multiplicities: &[2, 2], + u_knots: &[0.0, 1.0], + v_knots: &[0.0, 1.0], + knot_type: KnotType::Unspecified, + weights: None, + uv_domain: None, + }) + .unwrap() + } + + #[test] + fn copies_b_splines_when_asked() { + let k = kernel(); + let s = Surface::from(bilinear()); + let r = s.approximate_surface(&k, 1e-3, 3, true).unwrap(); + assert_eq!(r.achieved_tol, 0.0); + assert!(r.surface.is_some()); + } + + #[test] + fn approximates_a_b_spline_exactly() { + // a bilinear patch elevated to bicubic is reproduced by its Hermite data + let k = kernel(); + let s = Surface::from(bilinear()); + let r = s.approximate_surface(&k, 1e-6, 3, false).unwrap(); + assert!( + r.achieved_tol >= 0.0 && r.achieved_tol < 1e-12, + "{}", + r.achieved_tol + ); + assert!(r.surface.is_some()); + } + + #[test] + fn approximates_an_offset_quarter_cylinder() { + // issue #1441: the offset of a quarter cylinder of radius 5 + let k = kernel(); + let cyl = Surface::from(Cone::new_cylinder_bounded( + Vec3::ZERO, + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(0.0, 1.0, 0.0), + 5.0, + 0.0, + 90.0, + 10.0, + false, + false, + false, + )); + let off = Surface::from(OffsetSurface::new(&k, 2.0, cyl, true, 0).unwrap()); + let tol = 1e-3; + let r = off.approximate_surface(&k, tol, 3, false).unwrap(); + assert!(r.failure.is_none()); + assert!( + r.achieved_tol >= 0.0 && r.achieved_tol <= tol, + "{}", + r.achieved_tol + ); + let s = r.surface.unwrap(); + let uv = off.natural_uv_domain_with_nurb(&k).unwrap().mid(); + let p = s.evaluate_point(&k, uv).unwrap(); + let q = off.evaluate_point(&k, uv).unwrap(); + assert!(p.distance(q) <= 2.0 * tol, "{}", p.distance(q)); + } + + #[test] + fn degree_elevate_and_insert_keep_the_shape() { + let mut s = bilinear(); + let before = s.evaluate_point(Point2::new(0.3, 0.7)).unwrap(); + s.degree_elevate(SurfParamType::U, 3).unwrap(); + s.degree_elevate(SurfParamType::V, 3).unwrap(); + assert_eq!(s.degree(SurfParamType::U), 3); + s.insert_knots(SurfParamType::U, &[0.5, 0.5, 0.5]).unwrap(); + assert_eq!(s.number_control_points(SurfParamType::U), 7); + let after = s.evaluate_point(Point2::new(0.3, 0.7)).unwrap(); + assert!(before.distance(after) < 1e-12); + s.remove_knots(1e-9, true, true, None, None).unwrap(); + assert_eq!(s.number_control_points(SurfParamType::U), 4); + } +} diff --git a/fixtures/surface.surface_approximate/cases.json b/fixtures/surface.surface_approximate/cases.json new file mode 100644 index 0000000000..8e2f21cbe4 --- /dev/null +++ b/fixtures/surface.surface_approximate/cases.json @@ -0,0 +1,1578 @@ +{ + "cases": [ + { + "name": "issue1441_offset_quarter_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 9876543.21, + 3, + 0, + 1, + 0, + 2.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "issue1441_offset_quarter_cylinder_tol1e-3", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + 2.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "offset_quarter_cylinder_inward", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + -2.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "quarter_cylinder_bare", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "quarter_cylinder_just_copy", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 1, + 0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "offset_quarter_cylinder_just_copy", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 1, + 1, + 0, + 2.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "offset_full_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.0001, + 4, + 0, + 1, + 0, + 0.5, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 3, + 0, + 360, + 4, + 0, + 0, + 0 + ] + } + }, + { + "name": "nested_offsets_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 2, + 0, + 0.5, + 0, + 0.25, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "offset_zero_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + 0.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "offset_sphere", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + 1.0, + 4, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + -90, + 360, + 90, + 5, + 0, + 0 + ] + } + }, + { + "name": "offset_sphere_level0_tight", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 1e-09, + 0, + 0, + 1, + 0, + 1.0, + 4, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + -90, + 360, + 90, + 5, + 0, + 0 + ] + } + }, + { + "name": "offset_torus", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + 0.5, + 5, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 360, + 360, + 4, + 1, + 0, + 0 + ] + } + }, + { + "name": "offset_plane", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + 1.0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 2, + 3 + ] + } + }, + { + "name": "plane_just_copy", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 2, + 3 + ] + } + }, + { + "name": "offset_cone", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + 0.25, + 3, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 2, + 1, + 0, + 180, + 3, + 0, + 0, + 0 + ] + } + }, + { + "name": "degree4_bspline_single_patch_starter", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.0001, + 3, + 0, + 0, + 0, + 4, + 1, + 5, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1.0, + 1, + 0, + 2, + 1.0, + 1, + 1, + 0, + 0.25, + 1, + 1, + 2, + 0.25, + 1, + 2, + 0, + 0.0, + 1, + 2, + 2, + 0.0, + 1, + 3, + 0, + 0.25, + 1, + 3, + 2, + 0.25, + 1, + 4, + 0, + 1.0, + 1, + 4, + 2, + 1.0, + 1 + ] + } + }, + { + "name": "offset_degree4_bspline", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + 0.3, + 0, + 4, + 1, + 5, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1.0, + 1, + 0, + 2, + 1.0, + 1, + 1, + 0, + 0.25, + 1, + 1, + 2, + 0.25, + 1, + 2, + 0, + 0.0, + 1, + 2, + 2, + 0.0, + 1, + 3, + 0, + 0.25, + 1, + 3, + 2, + 0.25, + 1, + 4, + 0, + 1.0, + 1, + 4, + 2, + 1.0, + 1 + ] + } + }, + { + "name": "simple_knot_cubic_patch_index", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 0, + 0, + 3, + 3, + 6, + 4, + 0, + 0, + 0, + 0, + 0, + 0.3333333333333333, + 0.6666666666666666, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0.0, + 1, + 0, + 1, + 0.0, + 1, + 0, + 2, + 0.0, + 1, + 0, + 3, + 0.0, + 1, + 1, + 0, + 0.0, + 1, + 1, + 1, + 0.1, + 1, + 1, + 2, + 0.2, + 1, + 1, + 3, + 0.0, + 1, + 2, + 0, + 0.0, + 1, + 2, + 1, + 0.2, + 1, + 2, + 2, + 0.1, + 1, + 2, + 3, + 0.0, + 1, + 3, + 0, + 0.0, + 1, + 3, + 1, + 0.0, + 1, + 3, + 2, + 0.0, + 1, + 3, + 3, + 0.0, + 1, + 4, + 0, + 0.0, + 1, + 4, + 1, + 0.1, + 1, + 4, + 2, + 0.2, + 1, + 4, + 3, + 0.0, + 1, + 5, + 0, + 0.0, + 1, + 5, + 1, + 0.2, + 1, + 5, + 2, + 0.1, + 1, + 5, + 3, + 0.0, + 1 + ] + } + }, + { + "name": "offset_simple_knot_cubic", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 0, + 0.2, + 0, + 3, + 3, + 6, + 4, + 0, + 0, + 0, + 0, + 0, + 0.3333333333333333, + 0.6666666666666666, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0.0, + 1, + 0, + 1, + 0.0, + 1, + 0, + 2, + 0.0, + 1, + 0, + 3, + 0.0, + 1, + 1, + 0, + 0.0, + 1, + 1, + 1, + 0.1, + 1, + 1, + 2, + 0.2, + 1, + 1, + 3, + 0.0, + 1, + 2, + 0, + 0.0, + 1, + 2, + 1, + 0.2, + 1, + 2, + 2, + 0.1, + 1, + 2, + 3, + 0.0, + 1, + 3, + 0, + 0.0, + 1, + 3, + 1, + 0.0, + 1, + 3, + 2, + 0.0, + 1, + 3, + 3, + 0.0, + 1, + 4, + 0, + 0.0, + 1, + 4, + 1, + 0.1, + 1, + 4, + 2, + 0.2, + 1, + 4, + 3, + 0.0, + 1, + 5, + 0, + 0.0, + 1, + 5, + 1, + 0.2, + 1, + 5, + 2, + 0.1, + 1, + 5, + 3, + 0.0, + 1 + ] + } + }, + { + "name": "rational_arc_extrusion_offset", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 1e-05, + 3, + 0, + 1, + 0, + 0.5, + 0, + 2, + 1, + 3, + 2, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 2, + 1, + 0.7071067811865476, + 0.7071067811865476, + 0.0, + 0.7071067811865476, + 0.7071067811865476, + 0.7071067811865476, + 1.4142135623730951, + 0.7071067811865476, + 0, + 1, + 0, + 1, + 0, + 1, + 2, + 1 + ] + } + }, + { + "name": "bspline_just_copy", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 1, + 0, + 0, + 3, + 3, + 6, + 4, + 0, + 0, + 0, + 0, + 0, + 0.3333333333333333, + 0.6666666666666666, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0.0, + 1, + 0, + 1, + 0.0, + 1, + 0, + 2, + 0.0, + 1, + 0, + 3, + 0.0, + 1, + 1, + 0, + 0.0, + 1, + 1, + 1, + 0.1, + 1, + 1, + 2, + 0.2, + 1, + 1, + 3, + 0.0, + 1, + 2, + 0, + 0.0, + 1, + 2, + 1, + 0.2, + 1, + 2, + 2, + 0.1, + 1, + 2, + 3, + 0.0, + 1, + 3, + 0, + 0.0, + 1, + 3, + 1, + 0.0, + 1, + 3, + 2, + 0.0, + 1, + 3, + 3, + 0.0, + 1, + 4, + 0, + 0.0, + 1, + 4, + 1, + 0.1, + 1, + 4, + 2, + 0.2, + 1, + 4, + 3, + 0.0, + 1, + 5, + 0, + 0.0, + 1, + 5, + 1, + 0.2, + 1, + 5, + 2, + 0.1, + 1, + 5, + 3, + 0.0, + 1 + ] + } + }, + { + "name": "offset_cylinder_tol_zero", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.0, + 2, + 0, + 1, + 0, + 1.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "offset_cylinder_huge_scale", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 1000.0, + 3, + 0, + 1, + 0, + 200000.0, + 2, + 100000.0, + -200000.0, + 300000.0, + 1, + 0, + 0, + 0, + 1, + 0, + 500000.0, + 0, + 90, + 1000000.0, + 0, + 0, + 0 + ] + } + }, + { + "name": "offset_cylinder_tiny_scale", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 1e-09, + 3, + 0, + 1, + 0, + 2e-06, + 2, + 1e-06, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5e-06, + 0, + 90, + 1e-05, + 0, + 0, + 0 + ] + } + }, + { + "name": "curve_bounded_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 1, + 0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "curve_bounded_implicit_offset_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 2, + 0, + 2.0, + 1, + 1, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "offset_of_curve_bounded_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 2, + 1, + 0, + 0, + 2.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "step_cylinder_cubic_starter", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 2, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "step_sphere", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 2, + 4, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + -90, + 360, + 90, + 5, + 0, + 0 + ] + } + }, + { + "name": "offset_of_step_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 2, + 2, + 0, + 1.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "volume_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 1, + 3, + 1, + 2, + 3, + 0, + 1, + 0, + -1, + 0, + 0, + 0, + 0, + 1, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "volume_offset_sphere", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 2, + 0, + 1.0, + 3, + 1, + 2, + 3, + 0, + 1, + 0, + -1, + 0, + 0, + 0, + 0, + 1, + 4, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + -90, + 360, + 90, + 5, + 0, + 0 + ] + } + }, + { + "name": "offset_of_volume_cylinder", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 0, + 2, + 3, + 1, + 2, + 3, + 0, + 1, + 0, + -1, + 0, + 0, + 0, + 0, + 1, + 0, + 2.0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "curve_bounded_just_copy", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 1, + 1, + 1, + 0, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "step_just_copy", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 1, + 1, + 2, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + }, + { + "name": "volume_just_copy", + "upstream_ref": "source/SMLib/src/SmSurface.cpp", + "inputs": { + "block": [ + 0.001, + 3, + 1, + 1, + 3, + 1, + 2, + 3, + 0, + 1, + 0, + -1, + 0, + 0, + 0, + 0, + 1, + 2, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 90, + 10, + 0, + 0, + 0 + ] + } + } + ] +} \ No newline at end of file diff --git a/harness/cpp/shim/surface_surface_approximate.cpp b/harness/cpp/shim/surface_surface_approximate.cpp new file mode 100644 index 0000000000..58dd6d9dd5 --- /dev/null +++ b/harness/cpp/shim/surface_surface_approximate.cpp @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: Apache-2.0 +// Unit surface.surface_approximate. Builds a real SmBSplineSurface, SmPlane, SmCylinder, +// SmCone, SmSphere, SmTorus or SmSurfOfRevolution from plain data, optionally wraps it in +// up to three SmOffsetSurface, SmCurveBoundedSurface, SmSTEPSurface or SmSrfInVolume +// objects, and runs SmSurface::ApproximateSurface on it. +// +// Input block: `tol level just_copy n_wraps wraps... kind` then the kind's data, as in +// surface_surface_assert_valid.cpp: +// 0 B-spline: p q nu nv rational knots_u[nu+p+1] knots_v[nv+q+1] pts[nu*nv*4] (row i = u) +// 1 plane: O[3] X[3] Y[3] scale[2] domain[4] +// 2 cylinder: O X Y r start end h swap inside mngc +// 3 cone: O X Y bot top start end h swap inside mngc +// 4 sphere: O X Y domain[4] r swap inside +// 5 torus: O X Y domain[4] major minor swap inside +// 6 revolution: O X Y domain[4] swap, generator: p n rational pts[n*4] knots[n+p+1] +// Wraps are applied innermost first, each owning its base: `0 d` SmOffsetSurface(d, base, +// TRUE, 0); `1 implicit` SmCurveBoundedSurface without curves; `2` SmSTEPSurface(base, +// TRUE); `3 O[3] X[3] Y[3] Z[3]` SmSrfInVolume(base, FALSE, SmTransform(O, X, Y, &Z), 3, 0) +// (both parts copied; an original is freed only when it was copied: a curve bounded or +// STEP base has no Copy, so the object keeps and owns the original). +// `tol` is passed as an SmApproxTol3d (fabs, SM_USE_DEFAULT = half the zone tolerance). +// +// Output: `status achieved has` then, when a surface came back, its `type` and its +// SmBSplineSurface part: `deg_u deg_v n_u n_v`, per direction `count knots[count] +// mults[count]`, then the control points `x y z w` (SM_CP_EUCLIDIAN_RATIONAL, u major). +#include "sc_shim.h" +#include "StdAfx.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +struct ApConePeek : SmCone +{ + ApConePeek(const SmPoint3d& o, const SmVector3d& x, const SmVector3d& y, double bot, double top, + double start, double end, double h, SmBoolean swap, SmBoolean inside, SmBoolean mngc) + : SmCone(o, x, y, bot, top, start, end, h, swap, inside, mngc) + { + } +}; + +struct ApRevolution : SmSurfOfRevolution +{ + ApRevolution(SmBSplineCurve* gen, const SmPoint3d& o, const SmVector3d& x, const SmVector3d& y, + const SmExtent2d& d, SmBoolean swap) + : SmSurfOfRevolution(gen, o, x, y, d, swap) + { + } +}; + +struct ApRd +{ + const double* in; + int64_t n; + int64_t at; + bool ok = true; + double num() + { + if (at >= n) + { + ok = false; + return 0.0; + } + return in[at++]; + } + int64_t idx() { return (int64_t)num(); } + SmBoolean b() { return num() != 0.0 ? TRUE : FALSE; } + SmVector3d vec() + { + double x = num(); + double y = num(); + double z = num(); + return SmVector3d(x, y, z); + } + SmExtent2d dom() + { + double a = num(); + double b2 = num(); + double c = num(); + double d = num(); + return SmExtent2d(a, b2, c, d); + } +}; + +gw_CPOINT ap_point(ApRd& rd, bool rational) +{ + gw_CPOINT p; + p.x = rd.num(); + p.y = rd.num(); + p.z = rd.num(); + double w = rd.num(); + p.w = rational ? w : NL_NOW; + return p; +} + +// The surface of kind `kind` read from `rd`; NULL on a bad block. +SmSurface* ap_build(ApRd& rd, SmContext& ctx, int64_t kind) +{ + if (kind == 0) + { + int64_t p = rd.idx(), q = rd.idx(), nu = rd.idx(), nv = rd.idx(); + bool rational = rd.num() != 0.0; + if (p < 1 || q < 1 || nu < 2 || nv < 2 || nu > 64 || nv > 64) + return NULL; + std::vector ku(nu + p + 1), kv(nv + q + 1); + for (auto& v : ku) + v = rd.num(); + for (auto& v : kv) + v = rd.num(); + std::vector pts(nu * nv); + for (auto& c : pts) + c = ap_point(rd, rational); + if (!rd.ok) + return NULL; + std::vector rows(nu); + for (int64_t i = 0; i < nu; i++) + rows[i] = &pts[i * nv]; + return new (ctx) SmBSplineSurface((ULONG)(nu - 1), (ULONG)(nv - 1), (short)p, (short)q, + (ULONG)(nu + p), (ULONG)(nv + q), rows.data(), ku.data(), + kv.data()); + } + if (kind < 1 || kind > 6) + return NULL; + SmPoint3d o = rd.vec(); + SmVector3d x = rd.vec(); + SmVector3d y = rd.vec(); + if (kind == 1) + { + double sx = rd.num(), sy = rd.num(); + SmExtent2d d = rd.dom(); + if (!rd.ok) + return NULL; + return new (ctx) SmPlane(o, x, y, SmVector2d(sx, sy), d, &ctx); + } + if (kind == 2) + { + double r = rd.num(), s0 = rd.num(), s1 = rd.num(), h = rd.num(); + SmBoolean swap = rd.b(), inside = rd.b(), mngc = rd.b(); + if (!rd.ok) + return NULL; + return new (ctx) SmCylinder(o, x, y, r, s0, s1, h, swap, inside, mngc, &ctx); + } + if (kind == 3) + { + double bot = rd.num(), top = rd.num(), s0 = rd.num(), s1 = rd.num(), h = rd.num(); + SmBoolean swap = rd.b(), inside = rd.b(), mngc = rd.b(); + if (!rd.ok) + return NULL; + return new (ctx) ApConePeek(o, x, y, bot, top, s0, s1, h, swap, inside, mngc); + } + if (kind == 4) + { + SmExtent2d d = rd.dom(); + double r = rd.num(); + SmBoolean swap = rd.b(), inside = rd.b(); + if (!rd.ok) + return NULL; + return new (ctx) SmSphere(o, x, y, d, r, swap, inside, NULL, &ctx); + } + if (kind == 5) + { + SmExtent2d d = rd.dom(); + double major = rd.num(), minor = rd.num(); + SmBoolean swap = rd.b(), inside = rd.b(); + if (!rd.ok) + return NULL; + return new (ctx) SmTorus(o, x, y, d, major, minor, swap, inside, NULL, &ctx); + } + SmExtent2d d = rd.dom(); + SmBoolean swap = rd.b(); + int64_t p = rd.idx(), cn = rd.idx(); + bool rational = rd.num() != 0.0; + if (p < 1 || cn < 2 || cn > 64) + return NULL; + std::vector pts(cn); + for (auto& c : pts) + c = ap_point(rd, rational); + std::vector k(cn + p + 1); + for (auto& v : k) + v = rd.num(); + if (!rd.ok) + return NULL; + SmBSplineCurve* gen = new (ctx) + SmBSplineCurve(3, (ULONG)(cn - 1), (ULONG)(cn + p), (short)p, pts.data(), k.data()); + return new (ctx) ApRevolution(gen, o, x, y, d, swap); +} + +// The SmBSplineSurface part of `s`: degrees, counts, knots and control points. +void ap_put(std::vector& v, SmBSplineSurface* s) +{ + ULONG nu = s->SmBSplineSurface::GetNumberControlPoints(SM_SP_U); + ULONG nv = s->SmBSplineSurface::GetNumberControlPoints(SM_SP_V); + v.push_back((double)s->SmBSplineSurface::GetDegree(SM_SP_U)); + v.push_back((double)s->SmBSplineSurface::GetDegree(SM_SP_V)); + v.push_back((double)nu); + v.push_back((double)nv); + for (int dir = 0; dir < 2; dir++) + { + SmTArray k; + SmTArray m; + SmStatus st = s->SmBSplineSurface::GetKnots(dir == 0 ? SM_SP_U : SM_SP_V, k, &m); + if (st != SM_SUCCESS) + { + v.push_back(-1.0); + continue; + } + v.push_back((double)k.GetSize()); + for (ULONG i = 0; i < k.GetSize(); i++) + v.push_back(k[i]); + for (ULONG i = 0; i < m.GetSize(); i++) + v.push_back((double)m[i]); + } + for (ULONG i = 0; i < nu; i++) + for (ULONG j = 0; j < nv; j++) + { + SmPoint3d p; + double w = 0.0; + SmStatus st = s->GetControlPoint(SM_CP_EUCLIDIAN_RATIONAL, i, j, p, w); + if (st != SM_SUCCESS) + p = SmPoint3d(0.0, 0.0, 0.0), w = -1.0; + v.insert(v.end(), {p.x, p.y, p.z, w}); + } +} +} // namespace + +SC_EXPORT int32_t sc_surface_surface_approximate_eval(const double* input, int64_t n, double* out, + int64_t cap, int64_t* out_len) +{ + SC_TRY + SmContext ctx; + ApRd rd{input, n, 0}; + double tol = rd.num(); + int64_t level = rd.idx(); + bool just_copy = rd.num() != 0.0; + int64_t n_wrap = rd.idx(); + if (!rd.ok || level < 0 || n_wrap < 0 || n_wrap > 3) + return SC_ERR_ARG; + struct Wrap + { + int64_t type; + double d[12]; + }; + std::vector wraps((size_t)n_wrap); + for (auto& w : wraps) + { + w.type = rd.idx(); + int64_t m = w.type == 0 || w.type == 1 ? 1 : w.type == 3 ? 12 : 0; + if (w.type < 0 || w.type > 3) + return SC_ERR_ARG; + for (int64_t i = 0; i < m; i++) + w.d[i] = rd.num(); + } + int64_t kind = rd.idx(); + SmSurface* surface = ap_build(rd, ctx, kind); + if (surface == NULL) + return SC_ERR_ARG; + if (!rd.ok || rd.at != n) + { + delete surface; + return SC_ERR_ARG; + } + for (const Wrap& w : wraps) + { + if (w.type == 0) + surface = new (ctx) SmOffsetSurface(w.d[0], *surface, TRUE, 0); + else if (w.type == 1) + { + SmTArray c3d, cuv; + SmBoolean implicit = w.d[0] != 0.0 ? TRUE : FALSE; + surface = new (ctx) SmCurveBoundedSurface(*surface, &c3d, &cuv, implicit); + } + else if (w.type == 2) + surface = new (ctx) SmSTEPSurface(*surface, TRUE); + else + { + SmVector3d o(w.d[0], w.d[1], w.d[2]), x(w.d[3], w.d[4], w.d[5]); + SmVector3d y(w.d[6], w.d[7], w.d[8]), z(w.d[9], w.d[10], w.d[11]); + SmTransform* t = new (ctx) SmTransform(&ctx, o, x, y, &z); + SmSurface* base = surface; + SmSrfInVolume* siv = new (ctx) SmSrfInVolume(*base, FALSE, *t, 3, 0, &ctx); + // Copy flag 3 makes the object own (and later delete) what it points to. A + // failed Copy (SmSurface's default for curve bounded and STEP surfaces) leaves + // it pointing to the original: free only the originals it replaced by copies. + if (siv->GetSurface() != base) + delete base; + if (siv->GetVolume() != t) + delete t; + surface = siv; + } + } + + SmBSplineSurface* result = NULL; + double achieved = 0.0; + SmStatus st = surface->ApproximateSurface(ctx, SmApproxTol3d(tol), achieved, (ULONG)level, + result, just_copy ? TRUE : FALSE); + std::vector v; + v.push_back(st == SM_SUCCESS ? 0.0 : (double)st); + v.push_back(achieved); + v.push_back(result != NULL ? 1.0 : 0.0); + if (result != NULL) + { + v.push_back((double)result->GetType()); + ap_put(v, result); + delete result; + } + delete surface; + return sc_copy_out(v.data(), (int64_t)v.size(), out, cap, out_len); + SC_CATCH +} diff --git a/plan/units/surface.surface_approximate.toml b/plan/units/surface.surface_approximate.toml new file mode 100644 index 0000000000..dc29620e0e --- /dev/null +++ b/plan/units/surface.surface_approximate.toml @@ -0,0 +1,12 @@ +id = "surface.surface_approximate" +upstream = ["source/SMLib/src/SmSurface.cpp", "source/SMLib/src/SmBSplineSurfaceNLib.cpp", "source/SMLib/inc/SmCubicBezierSurface.h"] +rust_module = "surface::surface::approximate" +depends_on = ["surface.surface_p1", "surface.surface_p2", "surface.offset_surface", "nurbs.b_spline_surface_p1", "nurbs.b_spline_surface_p2", "nurbs.b_spline_surface_n_lib_p1", "nurbs.b_spline_surface_n_lib_p2", "nlib.srf_tool_p1", "nlib.srf_tool_p2", "nlib.srf_tool_p3", "nlib.srf_tool_p4"] +soft_depends_on = [] +est_lines = 640 +layer = 32 +tests = [] +status = "todo" +claimed_by = "" +claimed_at = "" +notes = "PR #1533, issue #1441 (offset surfaces cannot be exported; solidcore 1.0.1 parity). NOT IN callgraph/units.json: SmSurface::ApproximateSurface and its own callees are in harness/callgraph/unused_functions.txt (no upstream test or SM_API entry point reaches them). Owns SmSurface::ApproximateSurface (SmSurface.cpp:877), SmSurface::ComputeCubicBezierApprox (SmSurface.cpp:8600), SmCubicBezierSurface (its points only), the GetRootSurface overrides (SmSurface, SmBSplineSurface, SmOffsetSurface, SmCurveBoundedSurface, SmSTEPSurface, SmSrfInVolume), and SmBSplineSurface::DegreeElevate, InsertKnots (SmTArray form, SmBSplineSurfaceNLib.cpp:6490) and RemoveKnots. Rust: Surface::approximate_surface / approximate_surface_ignoring_status / compute_cubic_bezier_approx, BSplineSurface::degree_elevate / insert_knots / remove_knots. Not ported (follow-ups): the callers SmBrepData's bWriteAsBSplines writer (brep_data_write writes native formats only), SmFace::ApproximateWithBSpline, SmFace::ApproximateSurface, SmSurface::ApproximateSurfaceLeastSqs, the graphics output; SmCubicBezierSurface's other members. Status pending the gate run." From 6102946cd242182b3e5a4742071c221d125719cf Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 29 Oct 2026 17:48:29 -0400 Subject: [PATCH 3/4] plan: surface.surface_approximate green Co-Authored-By: Claude Opus 5.5 (1M context) --- plan/units/surface.surface_approximate.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plan/units/surface.surface_approximate.toml b/plan/units/surface.surface_approximate.toml index dc29620e0e..7bf2857425 100644 --- a/plan/units/surface.surface_approximate.toml +++ b/plan/units/surface.surface_approximate.toml @@ -6,7 +6,7 @@ soft_depends_on = [] est_lines = 640 layer = 32 tests = [] -status = "todo" +status = "green" claimed_by = "" claimed_at = "" notes = "PR #1533, issue #1441 (offset surfaces cannot be exported; solidcore 1.0.1 parity). NOT IN callgraph/units.json: SmSurface::ApproximateSurface and its own callees are in harness/callgraph/unused_functions.txt (no upstream test or SM_API entry point reaches them). Owns SmSurface::ApproximateSurface (SmSurface.cpp:877), SmSurface::ComputeCubicBezierApprox (SmSurface.cpp:8600), SmCubicBezierSurface (its points only), the GetRootSurface overrides (SmSurface, SmBSplineSurface, SmOffsetSurface, SmCurveBoundedSurface, SmSTEPSurface, SmSrfInVolume), and SmBSplineSurface::DegreeElevate, InsertKnots (SmTArray form, SmBSplineSurfaceNLib.cpp:6490) and RemoveKnots. Rust: Surface::approximate_surface / approximate_surface_ignoring_status / compute_cubic_bezier_approx, BSplineSurface::degree_elevate / insert_knots / remove_knots. Not ported (follow-ups): the callers SmBrepData's bWriteAsBSplines writer (brep_data_write writes native formats only), SmFace::ApproximateWithBSpline, SmFace::ApproximateSurface, SmSurface::ApproximateSurfaceLeastSqs, the graphics output; SmCubicBezierSurface's other members. Status pending the gate run." From fa675aef3fd90855c57e1da464a549ac9d6e0f1c Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 29 Oct 2026 19:40:18 -0400 Subject: [PATCH 4/4] surface: natural_uv_domain_with_nurb follows CB/STEP/in-volume wraps to the base (gen0199); SM_UNDEF twist where upstream leaves it unwritten natural_uv_domain_with_nurb sent curve-bounded, STEP and in-volume surfaces to natural_uv_domain_if_built, so a lazy nested NURB gave the empty (0,0)-(0,0) domain. Follow upstream's virtual chain (CB -> basis, SrfInVolume -> base, STEP -> base's GetSTEPUVDomain), with a bounded walk. Local: PASS surface.surface_approximate cases=436 passed=434 failed=0 deferred=0 rejected=2. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/surface/surface/approximate.rs | 52 +++++++++++++- crates/solidcore/src/surface/surface/p1.rs | 70 ++++++++++++++++++- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/crates/solidcore/src/surface/surface/approximate.rs b/crates/solidcore/src/surface/surface/approximate.rs index ffd6e770ad..608499b4ac 100644 --- a/crates/solidcore/src/surface/surface/approximate.rs +++ b/crates/solidcore/src/surface/surface/approximate.rs @@ -65,6 +65,7 @@ use crate::geom::extent2d::Extent2d; use crate::geom::vector2d::Point2; use crate::geom::vector3d::{Point3, Vec3}; use crate::kernel::Kernel; +use crate::math::UNDEF_DOUBLE; use crate::nurbs::b_spline_curve::{ControlPointForm, KnotType}; use crate::nurbs::b_spline_surface::{ BSplineSurface, BSplineSurfaceForm, CanonicalSurfaceData, SurfParamType, @@ -505,6 +506,11 @@ impl Surface { non_zero_tangents: false, }; let size = uv_domain.size(); + // `SmVector3d sMat[2][2]` starts at SM_UNDEF_DOUBLE (its members' default + // initializers); an evaluation that writes only the upper half leaves the twist + // `sMat[1][1]` at that value for every corner + let writes_twist = evaluation_writes_twist(self)?; + let undef = Vec3::new(UNDEF_DOUBLE, UNDEF_DOUBLE, UNDEF_DOUBLE); let corner = |uv: Point2| -> Result<[[Vec3; 2]; 2]> { let m = self.evaluate(k, uv, 1, 1, flags)?; let at = |i: usize, j: usize| -> Result { @@ -513,7 +519,8 @@ impl Surface { .copied() .ok_or_else(|| SolidError::sm_err(OP)) }; - Ok([[at(0, 0)?, at(0, 1)?], [at(1, 0)?, at(1, 1)?]]) + let twist = if writes_twist { at(1, 1)? } else { undef }; + Ok([[at(0, 0)?, at(0, 1)?], [at(1, 0)?, twist]]) }; let mut cb = CubicBezierSurface::default(); let p = &mut cb.points; @@ -635,6 +642,49 @@ impl Surface { } } +/// Whether `Evaluate(uv, 1, 1, .., bOnlyUpperHalf = FALSE, ..)` of `s` writes the +/// twist `D[1][1]`. A curve bounded surface passes the call to its basis surface and a +/// STEP surface to its base's `EvaluateSTEP`; there a curve bounded surface passes it +/// along again, the cone, sphere, torus and surface of revolution overrides write the +/// upper half only (`D[1][1]` needs `min(u, v) >= 2`), the surface of extrusion +/// override zeroes the whole array first, and every other kind evaluates in full +/// (`EvaluateSTEP`'s default is `Evaluate`). +/// +/// # Errors +/// +/// `InvalidInput` for a chain deeper than [`MAX_GEOMETRY_NESTING`] or a STEP surface +/// without base surface. +fn evaluation_writes_twist(s: &Surface) -> Result { + const OP: &str = OP_BEZIER; + let mut cur = s; + let mut step = false; + for _ in 0..=MAX_GEOMETRY_NESTING { + cur = match cur.kind() { + SurfaceKind::CurveBounded(c) => c.basis(), + // (a STEP surface's own `EvaluateSTEP` is the default, `Evaluate`: its base's + // `EvaluateSTEP` again) + SurfaceKind::Step(st) => { + step = true; + st.surface() + .ok_or_else(|| SolidError::invalid_input(OP, "STEP surface without base"))? + } + SurfaceKind::Cone(_) + | SurfaceKind::Sphere(_) + | SurfaceKind::Torus(_) + | SurfaceKind::SurfOfRevolution(_) + if step => + { + return Ok(false); + } + _ => return Ok(true), + }; + } + Err(SolidError::invalid_input( + OP, + "surface chain deeper than MAX_GEOMETRY_NESTING", + )) +} + impl BSplineSurface { /// Raises the degree in `param`'s direction to `new_degree` (`DegreeElevate`). /// diff --git a/crates/solidcore/src/surface/surface/p1.rs b/crates/solidcore/src/surface/surface/p1.rs index 00e1b7cfae..57fd356030 100644 --- a/crates/solidcore/src/surface/surface/p1.rs +++ b/crates/solidcore/src/surface/surface/p1.rs @@ -155,6 +155,7 @@ use crate::geom::vector3d::{Point3, Vec3}; use crate::intersect::surface_silhouette::SurfaceSilhouette; use crate::intersect::surface_tracer::SurfaceTrace; use crate::kernel::Kernel; + #[cfg(test)] use crate::math::EFF_ZERO_DEG; use crate::math::{max2, min2, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQ, EFF_ZERO_SQRT, PI}; @@ -2119,13 +2120,66 @@ impl Surface { /// surface's) the same way ([`OffsetSurface::natural_uv_domain_with_nurb`]), and so /// does a lazy surface of extrusion (`SmSurfOfExtrusion` does not override /// `GetNaturalUVDomain`: its made NURB's domain, whatever the generator's kind). - /// Every other kind answers as [`Surface::natural_uv_domain_if_built`]. + /// A curve bounded surface and a surface in a volume pass the call to their basis / + /// base surface, and a STEP surface answers its base's STEP domain + /// ([`Surface::step_uv_domain_with_nurb`]), so a lazy analytic under them is made + /// as upstream's virtual calls make it. Every other kind answers as + /// [`Surface::natural_uv_domain_if_built`]. /// /// # Errors /// - /// [`SolidError::Unported`] from the make. + /// [`SolidError::Unported`] from the make; `InvalidInput` for a curve bounded, STEP + /// or volume chain deeper than [`MAX_GEOMETRY_NESTING`](crate::surface::surface::p2::MAX_GEOMETRY_NESTING). pub fn natural_uv_domain_with_nurb(&self, k: &Kernel) -> Result { + self.natural_uv_domain_with_nurb_at(k, 0) + } + + /// [`Surface::step_uv_domain`] as upstream's `GetSTEPUVDomain`, whose base default + /// is the virtual `GetNaturalUVDomain` ([`Surface::natural_uv_domain_with_nurb`]): + /// the kinds with their own STEP domain answer it, a surface in a volume its base's, + /// the others their natural domain with a lazy NURB made. + /// + /// # Errors + /// + /// As [`Surface::natural_uv_domain_with_nurb`]. + pub fn step_uv_domain_with_nurb(&self, k: &Kernel) -> Result { + self.step_uv_domain_with_nurb_at(k, 0) + } + + /// [`Surface::step_uv_domain_with_nurb`] at chain depth `depth`. + fn step_uv_domain_with_nurb_at(&self, k: &Kernel, depth: usize) -> Result { + Ok(match self.kind() { + SurfaceKind::Plane(_) + | SurfaceKind::Cone(_) + | SurfaceKind::Sphere(_) + | SurfaceKind::Torus(_) + | SurfaceKind::SurfOfRevolution(_) + | SurfaceKind::SurfOfExtrusion(_) => self.step_uv_domain(), + SurfaceKind::SrfInVolume(s) => s.surface().step_uv_domain_with_nurb_at( + k, + nesting_depth(depth, "surface::surface::p1::step_uv_domain_with_nurb")?, + )?, + SurfaceKind::BSpline(_) + | SurfaceKind::CurveBounded(_) + | SurfaceKind::Step(_) + | SurfaceKind::OffsetSurface(_) => self.natural_uv_domain_with_nurb_at(k, depth)?, + }) + } + + /// [`Surface::natural_uv_domain_with_nurb`] at chain depth `depth`. + fn natural_uv_domain_with_nurb_at(&self, k: &Kernel, depth: usize) -> Result { + const OP: &str = "surface::surface::p1::natural_uv_domain_with_nurb"; Ok(match self.kind_with_nurb(k)? { + SurfaceKind::CurveBounded(s) => s + .basis() + .natural_uv_domain_with_nurb_at(k, nesting_depth(depth, OP)?)?, + SurfaceKind::SrfInVolume(s) => s + .surface() + .natural_uv_domain_with_nurb_at(k, nesting_depth(depth, OP)?)?, + SurfaceKind::Step(s) => match s.surface() { + Some(base) => base.step_uv_domain_with_nurb_at(k, nesting_depth(depth, OP)?)?, + None => s.natural_uv_domain(), + }, SurfaceKind::Cone(s) => s.rev().base().natural_uv_domain(), SurfaceKind::SurfOfRevolution(s) => s.base().natural_uv_domain(), SurfaceKind::Sphere(s) => s.base().base().natural_uv_domain(), @@ -4259,6 +4313,18 @@ impl<'a> SssIntersectEnfo<'a> { } } +/// The next depth of a curve bounded, STEP or volume chain walk, `InvalidInput` past +/// [`MAX_GEOMETRY_NESTING`](crate::surface::surface::p2::MAX_GEOMETRY_NESTING). +fn nesting_depth(depth: usize, op: &'static str) -> Result { + if depth >= crate::surface::surface::p2::MAX_GEOMETRY_NESTING { + return Err(SolidError::invalid_input( + op, + "surface chain deeper than MAX_GEOMETRY_NESTING", + )); + } + Ok(depth + 1) +} + #[cfg(test)] mod tests { use super::*;