solidcraft-smlib: ssi hands out SMLib's intersection curves exactly, as schema 4 B-splines

ssi::intersect took a sample count and gave each intersection curve as
samples: its 3D points, its UV points on each surface and the surfaces at
them. The fork's 947eedc hands out the curves SMLib's intersectors return,
exactly (smlib_ssi_curve_nurbs: degree, knots, control points, weights), and
drops the samples; SMLIB_ABI_VERSION 4.

SsiCurve now holds the 3D curve as a schema 4 Curve::Spline or
Curve::RationalSpline (rational where a weight is not 1) and each UV curve
the method returned as a Curve2, with SMLib's type, deviation estimate and
the 3D curve's natural interval; ssi::intersect loses its sample count. A
curve SMLib does not hold as a B-spline is Error::Unsupported (it is never
approximated); a B-spline whose knot count is not points + degree + 1 is an
argument error.

The SSI corpus scorer (tests/ssi_corpus.rs) samples the exact curves itself
where the corpus runner samples them: 4001 parameters spread evenly over the
3D curve's natural interval and over each UV curve's interval, the surfaces
evaluated at the UV samples, samples that cannot be evaluated dropped. The
unit test plane_through_sphere_is_a_circle evaluates the exact curve.

Nothing else used the samples. SMLIB_REV: the fork at 947eedc.

clippy -p solidcraft-smlib --all-targets, with and without native: clean.
The corpus gate (smlib_global_intersect_at_1e_6) is not run here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 07:19:00 -04:00
parent c83bd8f94b
commit c3f99d0e90
5 changed files with 154 additions and 123 deletions
+1 -1
View File
@@ -1 +1 @@
4a81dd9ca9a7bbd41c5125e4cc7b026f6abae5e5
f6d2c5e00e63b0923351566714f161decf64373d
+1 -1
View File
@@ -7,7 +7,7 @@
use std::ffi::{c_char, c_void};
/// `SMLIB_ABI_VERSION` this crate was written against.
pub const ABI_VERSION: i32 = 3;
pub const ABI_VERSION: i32 = 4;
pub const OK: i32 = 0;
pub const ERR_EXCEPTION: i32 = -1000;
+55 -45
View File
@@ -70,27 +70,22 @@ unsafe extern "C" {
fn smlib_surface_free(s: *mut RawSurface);
fn smlib_surface_domain(s: *const RawSurface, out: *mut f64) -> i32;
fn smlib_surface_evaluate(s: *const RawSurface, u: f64, v: f64, out: *mut f64) -> i32;
fn smlib_ssi(a: *const RawSurface, b: *const RawSurface, method: i32, approx_tol: f64, samples_per_curve: i32, out: *mut *mut RawResult) -> i32;
fn smlib_ssi(a: *const RawSurface, b: *const RawSurface, method: i32, approx_tol: f64, out: *mut *mut RawResult) -> i32;
fn smlib_ssi_result_free(r: *mut RawResult);
fn smlib_ssi_curve_count(r: *const RawResult, out: *mut i32) -> i32;
fn smlib_ssi_curve_info(
fn smlib_ssi_curve_info(r: *const RawResult, i: i32, ty: *mut i32, flags: *mut i32, deviation: *mut f64, interval: *mut f64) -> i32;
fn smlib_ssi_curve_nurbs(
r: *const RawResult,
i: i32,
ty: *mut i32,
flags: *mut i32,
samples: *mut i32,
deviation: *mut f64,
interval: *mut f64,
) -> i32;
fn smlib_ssi_curve_samples(
r: *const RawResult,
i: i32,
cap: i64,
xyz: *mut f64,
uv_a: *mut f64,
uv_b: *mut f64,
a_at_uv: *mut f64,
b_at_uv: *mut f64,
which: i32,
degree: *mut i32,
n_points: *mut i64,
n_knots: *mut i64,
cap_points: i64,
points: *mut f64,
weights: *mut f64,
cap_knots: i64,
knots: *mut f64,
) -> i32;
fn smlib_ssi_point_count(r: *const RawResult, out: *mut i32) -> i32;
fn smlib_ssi_point(r: *const RawResult, i: i32, xyz: *mut f64, uv_a: *mut f64, uv_b: *mut f64) -> i32;
@@ -139,20 +134,22 @@ impl Drop for ResultHandle {
#[derive(Clone, Copy, Debug)]
pub struct CurveInfo {
pub ty: i32,
pub flags: i32,
pub samples: i32,
pub deviation: f64,
pub interval: [f64; 2],
}
/// One curve's samples, flat: 3 (xyz, a_at_uv, b_at_uv) or 2 (uv_a, uv_b) values per sample.
/// `smlib_ssi_curve_nurbs` `which`: the 3D curve, the UV curve on a, on b.
pub const CURVE_3D: i32 = 0;
pub const CURVE_UV_A: i32 = 1;
pub const CURVE_UV_B: i32 = 2;
/// One B-spline as `smlib_ssi_curve_nurbs` gives it: cartesian control points, `dim` values each,
/// one weight each, and every knot (as many as points + degree + 1).
#[derive(Clone, Debug, Default)]
pub struct Samples {
pub xyz: Vec<f64>,
pub uv_a: Vec<f64>,
pub uv_b: Vec<f64>,
pub a_at_uv: Vec<f64>,
pub b_at_uv: Vec<f64>,
pub struct Nurbs {
pub points: Vec<f64>,
pub weights: Vec<f64>,
pub knots: Vec<f64>,
}
#[cfg(feature = "native")]
@@ -258,9 +255,9 @@ mod calls {
st => Err(st),
}
}
pub fn ssi(a: &SurfaceHandle, b: &SurfaceHandle, method: i32, approx_tol: f64, samples: i32) -> Result<ResultHandle, i32> {
pub fn ssi(a: &SurfaceHandle, b: &SurfaceHandle, method: i32, approx_tol: f64) -> Result<ResultHandle, i32> {
let mut out: *mut RawResult = std::ptr::null_mut();
match (unsafe { smlib_ssi(a.0.as_ptr(), b.0.as_ptr(), method, approx_tol, samples, &mut out) }, std::ptr::NonNull::new(out)) {
match (unsafe { smlib_ssi(a.0.as_ptr(), b.0.as_ptr(), method, approx_tol, &mut out) }, std::ptr::NonNull::new(out)) {
(OK, Some(p)) => Ok(ResultHandle(p)),
(OK, None) => Err(ERR_EMPTY),
(s, _) => Err(s),
@@ -274,30 +271,43 @@ mod calls {
}
}
pub fn curve_info(r: &ResultHandle, i: i32) -> Result<CurveInfo, i32> {
let mut c = CurveInfo { ty: 0, flags: 0, samples: 0, deviation: 0.0, interval: [0.0; 2] };
match unsafe { smlib_ssi_curve_info(r.0.as_ptr(), i, &mut c.ty, &mut c.flags, &mut c.samples, &mut c.deviation, c.interval.as_mut_ptr()) } {
let mut c = CurveInfo { ty: 0, deviation: 0.0, interval: [0.0; 2] };
match unsafe { smlib_ssi_curve_info(r.0.as_ptr(), i, &mut c.ty, std::ptr::null_mut(), &mut c.deviation, c.interval.as_mut_ptr()) } {
OK => Ok(c),
s => Err(s),
}
}
pub fn curve_samples(r: &ResultHandle, i: i32, n: usize) -> Result<Samples, i32> {
let cap = i64::try_from(n).map_err(|_| ERR_ARG)?;
let three = n.checked_mul(3).ok_or(ERR_ARG)?;
let mut s =
Samples { xyz: vec![0.0; three], uv_a: vec![0.0; 2 * n], uv_b: vec![0.0; 2 * n], a_at_uv: vec![0.0; three], b_at_uv: vec![0.0; three] };
/// Curve `i`'s `which` B-spline, `dim` coordinates per control point: the counts first, then
/// the arrays.
pub fn curve_nurbs(r: &ResultHandle, i: i32, which: i32, dim: usize) -> Result<Nurbs, i32> {
let (mut degree, mut np, mut nk) = (0i32, 0i64, 0i64);
let null = std::ptr::null_mut();
let st = unsafe { smlib_ssi_curve_nurbs(r.0.as_ptr(), i, which, &mut degree, &mut np, &mut nk, 0, null, null, 0, null) };
if st != OK {
return Err(st);
}
let (n, k) = (usize::try_from(np).map_err(|_| ERR_ARG)?, usize::try_from(nk).map_err(|_| ERR_ARG)?);
let degree = usize::try_from(degree).map_err(|_| ERR_ARG)?;
if !(n > degree && k == n + degree + 1) {
return Err(ERR_ARG);
}
let mut c = Nurbs { points: vec![0.0; n.checked_mul(dim).ok_or(ERR_ARG)?], weights: vec![0.0; n], knots: vec![0.0; k] };
let st = unsafe {
smlib_ssi_curve_samples(
smlib_ssi_curve_nurbs(
r.0.as_ptr(),
i,
cap,
s.xyz.as_mut_ptr(),
s.uv_a.as_mut_ptr(),
s.uv_b.as_mut_ptr(),
s.a_at_uv.as_mut_ptr(),
s.b_at_uv.as_mut_ptr(),
which,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
np,
c.points.as_mut_ptr(),
c.weights.as_mut_ptr(),
nk,
c.knots.as_mut_ptr(),
)
};
if st == OK { Ok(s) } else { Err(st) }
if st == OK { Ok(c) } else { Err(st) }
}
pub fn point_count(r: &ResultHandle) -> Result<i32, i32> {
let mut n = 0;
@@ -345,7 +355,7 @@ mod calls {
pub fn evaluate(_: &SurfaceHandle, _: f64, _: f64) -> Result<[f64; 3], i32> {
Err(NOT_BUILT)
}
pub fn ssi(_: &SurfaceHandle, _: &SurfaceHandle, _: i32, _: f64, _: i32) -> Result<ResultHandle, i32> {
pub fn ssi(_: &SurfaceHandle, _: &SurfaceHandle, _: i32, _: f64) -> Result<ResultHandle, i32> {
Err(NOT_BUILT)
}
pub fn curve_count(_: &ResultHandle) -> Result<i32, i32> {
@@ -354,7 +364,7 @@ mod calls {
pub fn curve_info(_: &ResultHandle, _: i32) -> Result<CurveInfo, i32> {
Err(NOT_BUILT)
}
pub fn curve_samples(_: &ResultHandle, _: i32, _: usize) -> Result<Samples, i32> {
pub fn curve_nurbs(_: &ResultHandle, _: i32, _: i32, _: usize) -> Result<Nurbs, i32> {
Err(NOT_BUILT)
}
pub fn point_count(_: &ResultHandle) -> Result<i32, i32> {
+55 -42
View File
@@ -1,15 +1,14 @@
//! Surface/surface intersection (`capi/include/smlib.h`, "Surface/surface intersection"): two
//! surfaces described in plain numbers, intersected by one of SMLib's SSI entry points, each
//! intersection curve sampled uniformly in its parameters.
//! intersection curve exactly as SMLib holds it: schema 4 B-splines (`solidcraft_geom::brep`).
//!
//! The constructors take exactly the fields of an SSI corpus case (`tests/ssi-corpus`), angles in
//! degrees. Each surface's UV domain is SMLib's natural one ([`Surface::domain`]): (u, v) itself
//! for planes, the parameterisation of SMLib's internal NURBS form for the other analytic kinds.
use crate::{Error, Result, error, ffi, init};
use solidcraft_geom::brep as b;
/// `SMLIB_SSI_MAX_SAMPLES`: most samples per curve.
pub const MAX_SAMPLES: usize = 100_000;
use crate::{Error, Result, error, ffi, init};
/// A surface with its UV domain, owned on the C side.
pub struct Surface(ffi::ssi::SurfaceHandle);
@@ -209,27 +208,25 @@ impl CurveType {
}
}
/// One intersection curve, sampled at the same count of parameters on the 3D curve and on each
/// UV curve. A value SMLib could not evaluate is NaN, and so is every UV value (with the surface
/// at it) of a UV curve the method did not return ([`SsiCurve::has_uv_a`]).
/// One intersection curve as SMLib returned it: its 3D curve and, where the method returned
/// them, its curves in each surface's UV, each the (rational) B-spline SMLib holds (its lines,
/// circles and ellipses are B-splines too; an intersection curve one SMLib fitted within the
/// approximation tolerance).
#[derive(Clone, Debug)]
pub struct SsiCurve {
pub curve_type: CurveType,
pub has_uv_a: bool,
pub has_uv_b: bool,
/// SMLib's deviation estimate (NaN: none).
pub deviation: f64,
/// The 3D curve's natural parameter interval, which `xyz` samples uniformly.
/// The 3D curve's natural parameter interval.
pub interval: [f64; 2],
pub xyz: Vec<[f64; 3]>,
pub uv_a: Vec<[f64; 2]>,
pub uv_b: Vec<[f64; 2]>,
/// Surface a at `uv_a`, b at `uv_b`.
pub a_at_uv: Vec<[f64; 3]>,
pub b_at_uv: Vec<[f64; 3]>,
pub curve: b::Curve,
/// On surface a, in a's UV.
pub uv_a: Option<b::Curve2>,
/// On surface b, in b's UV.
pub uv_b: Option<b::Curve2>,
}
/// An isolated point: a [`CurveType::Touching`] curve's first sample.
/// An isolated point: where a [`CurveType::Touching`] curve starts.
#[derive(Clone, Copy, Debug)]
pub struct SsiPoint {
pub xyz: [f64; 3],
@@ -244,39 +241,54 @@ pub struct SsiResult {
pub points: Vec<SsiPoint>,
}
fn triples(v: &[f64]) -> Vec<[f64; 3]> {
v.as_chunks::<3>().0.to_vec()
/// The 3D B-spline: polynomial where every weight is 1.
fn curve3(c: ffi::ssi::Nurbs) -> b::Curve {
let points = c.points.as_chunks::<3>().0;
if c.weights.iter().all(|w| *w == 1.0) {
b::Curve::Spline { knots: c.knots, points: points.to_vec() }
} else {
b::Curve::RationalSpline {
knots: c.knots,
homogeneous: points.iter().zip(&c.weights).map(|(p, w)| [p[0] * w, p[1] * w, p[2] * w, *w]).collect(),
}
}
}
fn pairs(v: &[f64]) -> Vec<[f64; 2]> {
v.as_chunks::<2>().0.to_vec()
/// A UV B-spline, as [`curve3`].
fn curve2(c: ffi::ssi::Nurbs) -> b::Curve2 {
let points = c.points.as_chunks::<2>().0;
if c.weights.iter().all(|w| *w == 1.0) {
b::Curve2::Spline { knots: c.knots, points: points.to_vec() }
} else {
b::Curve2::RationalSpline { knots: c.knots, homogeneous: points.iter().zip(&c.weights).map(|(p, w)| [p[0] * w, p[1] * w, *w]).collect() }
}
}
/// A UV curve the method may not have returned (`SMLIB_ERR_EMPTY`).
fn uv(r: &ffi::ssi::ResultHandle, i: i32, which: i32) -> Result<Option<b::Curve2>> {
match ffi::ssi::curve_nurbs(r, i, which, 2) {
Ok(c) => Ok(Some(curve2(c))),
Err(ffi::ERR_EMPTY) => Ok(None),
Err(s) => Err(error(s)),
}
}
/// Intersects `a` and `b` within their domains with `method` at approximation tolerance
/// `approx_tol` (<= 0: SMLib's default, 1/1000 of the surfaces' size), sampling each curve at
/// `samples` parameters (2..=[`MAX_SAMPLES`]). An intersector failure is SMLib's status.
pub fn intersect(a: &Surface, b: &Surface, method: Method, approx_tol: f64, samples: usize) -> Result<SsiResult> {
if !(2..=MAX_SAMPLES).contains(&samples) {
return Err(Error::Argument);
}
let r = ffi::ssi::ssi(&a.0, &b.0, method.code(), approx_tol, int(samples)?).map_err(error)?;
/// `approx_tol` (<= 0: SMLib's default, 1/1000 of the surfaces' size). An intersector failure is
/// SMLib's status; a curve SMLib does not hold as a B-spline is [`Error::Unsupported`].
pub fn intersect(a: &Surface, b: &Surface, method: Method, approx_tol: f64) -> Result<SsiResult> {
let r = ffi::ssi::ssi(&a.0, &b.0, method.code(), approx_tol).map_err(error)?;
let n = ffi::ssi::curve_count(&r).map_err(error)?;
let mut curves = Vec::new();
for i in 0..n {
let info = ffi::ssi::curve_info(&r, i).map_err(error)?;
let count = usize::try_from(info.samples).map_err(|_| Error::Argument)?;
let s = ffi::ssi::curve_samples(&r, i, count).map_err(error)?;
curves.push(SsiCurve {
curve_type: CurveType::from_code(info.ty),
has_uv_a: info.flags & 1 != 0,
has_uv_b: info.flags & 2 != 0,
deviation: info.deviation,
interval: info.interval,
xyz: triples(&s.xyz),
uv_a: pairs(&s.uv_a),
uv_b: pairs(&s.uv_b),
a_at_uv: triples(&s.a_at_uv),
b_at_uv: triples(&s.b_at_uv),
curve: curve3(ffi::ssi::curve_nurbs(&r, i, ffi::ssi::CURVE_3D, 3).map_err(error)?),
uv_a: uv(&r, i, ffi::ssi::CURVE_UV_A)?,
uv_b: uv(&r, i, ffi::ssi::CURVE_UV_B)?,
});
}
let n = ffi::ssi::point_count(&r).map_err(error)?;
@@ -304,15 +316,16 @@ mod tests {
(Ok(p), Ok(s)) => (p, s),
(p, s) => panic!("{p:?} {s:?}"),
};
let r = intersect(&plane, &sphere, Method::Global, 1e-6, 101).expect("intersect");
let r = intersect(&plane, &sphere, Method::Global, 1e-6).expect("intersect");
assert!(!r.curves.is_empty());
for c in &r.curves {
assert_eq!(c.xyz.len(), 101);
for p in &c.xyz {
assert!((p[0].hypot(p[1]) - 1.0).abs() < 1e-5 && p[2].abs() < 1e-5, "{p:?}");
let eval = b::eval::CurveEval::new(&c.curve).expect("a curve");
let [t0, t1] = c.interval;
for k in 0..=100 {
let p = eval.point(t0 + (t1 - t0) * f64::from(k) / 100.0).expect("evaluates");
assert!((p.x.hypot(p.y) - 1.0).abs() < 1e-5 && p.z.abs() < 1e-5, "{p:?}");
}
}
assert_eq!(intersect(&plane, &sphere, Method::Global, 1e-6, 1).err(), Some(Error::Argument));
}
#[test]
+42 -34
View File
@@ -530,12 +530,20 @@ fn surface(s: &Value) -> Result<solidcraft_smlib::ssi::Surface, String> {
r.map_err(|e| e.to_string())
}
/// SMLib's answer as the corpus runner writes it for `gsi`: samples SMLib could not evaluate
/// dropped, empty UV arrays for a UV curve the method did not return, the runner's type names,
/// and no isolated points (the runner only reports those in face mode).
/// SMLib's answer as the corpus runner writes it for `gsi`: each exact curve sampled where the
/// runner samples it ([`RUNNER_SAMPLES`] parameters spread evenly over the 3D curve's natural
/// interval, each UV curve over its own, the surfaces at the UV samples), samples that cannot be
/// evaluated dropped, empty UV arrays for a UV curve the method did not return, the runner's type
/// names, and no isolated points (the runner only reports those in face mode).
#[cfg(feature = "native")]
fn runner_output(r: solidcraft_smlib::Result<solidcraft_smlib::ssi::SsiResult>, ms: f64) -> Outcome {
use solidcraft_smlib::ssi::CurveType;
fn runner_output(
r: solidcraft_smlib::Result<solidcraft_smlib::ssi::SsiResult>,
a: &solidcraft_smlib::ssi::Surface,
b: &solidcraft_smlib::ssi::Surface,
ms: f64,
) -> Outcome {
use solidcraft_geom::brep::{Curve2, eval::CurveEval};
use solidcraft_smlib::ssi::{CurveType, Surface};
let r = match r {
Ok(r) => r,
Err(solidcraft_smlib::Error::Status(s)) => {
@@ -543,33 +551,33 @@ fn runner_output(r: solidcraft_smlib::Result<solidcraft_smlib::ssi::SsiResult>,
}
Err(e) => return Outcome::Crashed(e.to_string()),
};
let curves = r
.curves
.into_iter()
.map(|c| {
let curve_type = match c.curve_type {
CurveType::None => "n/a",
CurveType::Unknown => "UNKNOWN",
CurveType::Touching => "TOUCHING",
CurveType::Crossing => "CROSSING",
CurveType::Tangent => "TANGENT",
CurveType::Coincident => "COINCIDENT",
CurveType::NearTangent => "NEAR_TANGENT",
CurveType::RegionBoundary => "REGION_BOUNDARY",
CurveType::Other(_) => "?",
};
let evaluated = |p: &[f64]| p.iter().any(|x| !x.is_nan());
let on = |has: bool, uv: Vec<[f64; 2]>, at: Vec<V3>| -> (Vec<[f64; 2]>, Vec<V3>) {
if !has {
return (Vec::new(), Vec::new());
}
uv.into_iter().zip(at).filter(|(q, _)| evaluated(q)).unzip()
};
let (uv1, s1_at_uv) = on(c.has_uv_a, c.uv_a, c.a_at_uv);
let (uv2, s2_at_uv) = on(c.has_uv_b, c.uv_b, c.b_at_uv);
EngineCurve { curve_type: curve_type.into(), xyz: c.xyz.into_iter().filter(|p| evaluated(p)).collect(), uv1, uv2, s1_at_uv, s2_at_uv }
})
.collect();
let params = |[t0, t1]: [f64; 2]| (0..RUNNER_SAMPLES).map(move |i| t0 + (t1 - t0) * (i as f64 / (RUNNER_SAMPLES - 1) as f64));
let on = |c: &Option<Curve2>, s: &Surface| -> (Vec<[f64; 2]>, Vec<V3>) {
let Some(c) = c else { return (Vec::new(), Vec::new()) };
let Some(ivl) = c.interval() else { return (Vec::new(), Vec::new()) };
params(ivl).filter_map(|t| c.eval(t)).filter_map(|q| s.evaluate(q[0], q[1]).ok().map(|p| (q, p))).unzip()
};
let mut curves = Vec::new();
for c in r.curves {
let curve_type = match c.curve_type {
CurveType::None => "n/a",
CurveType::Unknown => "UNKNOWN",
CurveType::Touching => "TOUCHING",
CurveType::Crossing => "CROSSING",
CurveType::Tangent => "TANGENT",
CurveType::Coincident => "COINCIDENT",
CurveType::NearTangent => "NEAR_TANGENT",
CurveType::RegionBoundary => "REGION_BOUNDARY",
CurveType::Other(_) => "?",
};
let xyz = match CurveEval::new(&c.curve) {
Ok(e) => params(c.interval).filter_map(|t| e.point(t)).map(|p| [p.x, p.y, p.z]).collect(),
Err(e) => return Outcome::Crashed(format!("an intersection curve: {e}")),
};
let (uv1, s1_at_uv) = on(&c.uv_a, a);
let (uv2, s2_at_uv) = on(&c.uv_b, b);
curves.push(EngineCurve { curve_type: curve_type.into(), xyz, uv1, uv2, s1_at_uv, s2_at_uv });
}
Outcome::Ran(EngineResult { status: ssi_score::SM_SUCCESS, status_name: None, time_ms: ms, curves, points: Vec::new() })
}
@@ -588,8 +596,8 @@ fn smlib_global_intersect_at_1e_6() {
let a = surface(&case.surfaces[0]).unwrap_or_else(|e| panic!("{} surface 1: {e}", case.id));
let b = surface(&case.surfaces[1]).unwrap_or_else(|e| panic!("{} surface 2: {e}", case.id));
let t = std::time::Instant::now();
let r = intersect(&a, &b, Method::Global, 1e-6, RUNNER_SAMPLES);
runner_output(r, t.elapsed().as_secs_f64() * 1e3)
let r = intersect(&a, &b, Method::Global, 1e-6);
runner_output(r, &a, &b, t.elapsed().as_secs_f64() * 1e3)
})
.collect();
let ran = t0.elapsed();