diff --git a/crates/difftest/src/units/primitive_api_curve_ops.rs b/crates/difftest/src/units/primitive_api_curve_ops.rs new file mode 100644 index 0000000000..dc932ecf5a --- /dev/null +++ b/crates/difftest/src/units/primitive_api_curve_ops.rs @@ -0,0 +1,668 @@ +//! Runner for unit `primitive.api_curve_ops`: the `SM_API` curve utilities +//! (`SmApiCreateCurveInterpPoints`, `SmApiOffsetCurve`, `SmApiEvaluateContinuity`, +//! `SmApiMakeCurvesCompatible`, `SmApiOrderCurves`). +//! +//! C++: `solidcore_ffi::units::primitive_api_curve_ops` (upstream +//! `source/SM_API/src/SmApiCurves.cpp`). Rust: `solidcore::primitive::api_curve_ops`. +//! +//! A case is one API call (`op` plus its arguments; floats may be `"NaN"`, `"inf"`, +//! `"-inf"`). Input curves are made by the `SM_API` curve constructors on each side +//! (`primitive.api_curves`); a constructor failure records `-2` alone. Both sides +//! produce the record documented in the ffi module: the status (`0` = `SM_SUCCESS`, +//! else the `SmStatus`; `1007` for a Rust `InvalidInput`, `-1` for a Rust error +//! upstream cannot produce), then the result, reported as the values `rec`. A +//! continuity parameter given as `relative` is the curve's natural interval evaluated +//! there (`SmExtent1d::Evaluate`), on each side from its own curve. A case whose Rust +//! side reaches an unported unit is deferred. + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; + +use solidcore::curve::curve::{Curve, CurveKind}; +use solidcore::geom::extent1d::Extent1d; +use solidcore::geom::vector3d::Vec3; +use solidcore::kernel::Kernel; +use solidcore::nurbs::b_spline_curve::BSplineCurve; +use solidcore::nurbs::crv_fit::p1::ParamMethod; +use solidcore::primitive::api_curve_ops as api; +use solidcore::primitive::api_curves; +use solidcore::SolidError; +use solidcore_ffi::units::primitive_api_curve_ops::{self as ffi, Call, InCurve}; + +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 = 600; + +/// `primitive.api_curve_ops` runner. +pub struct Runner; + +// ---- inputs ------------------------------------------------------------------------- + +/// A float that may be written `"NaN"`, `"inf"` or `"-inf"` in JSON. +#[derive(Debug, Clone, Copy, PartialEq)] +struct F(f64); + +impl Serialize for F { + fn serialize(&self, s: S) -> Result { + if self.0.is_nan() { + s.serialize_str("NaN") + } else if self.0 == f64::INFINITY { + s.serialize_str("inf") + } else if self.0 == f64::NEG_INFINITY { + s.serialize_str("-inf") + } else { + s.serialize_f64(self.0) + } + } +} + +impl<'de> Deserialize<'de> for F { + fn deserialize>(d: D) -> Result { + match Value::deserialize(d)? { + Value::Number(n) => n + .as_f64() + .map(F) + .ok_or_else(|| serde::de::Error::custom("bad number")), + Value::String(s) if s == "NaN" => Ok(F(f64::NAN)), + Value::String(s) if s == "inf" => Ok(F(f64::INFINITY)), + Value::String(s) if s == "-inf" => Ok(F(f64::NEG_INFINITY)), + other => Err(serde::de::Error::custom(format!("bad float {other}"))), + } + } +} + +type P3 = [F; 3]; + +/// One input curve, made by an `SM_API` constructor. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "kind")] +enum Crv { + Curve { points: Vec }, + Circle { center: P3, radius: F }, + Arc { center: P3, radius: F, start_deg: F, end_deg: F }, + Segment { start: P3, end: P3 }, + AnalyticCircle { center: P3, radius: F }, + Rectangle { center: P3, length: F, width: F }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "op")] +enum In { + Interp { + points: Vec, + /// `SmCurveParameterizationType` code: 0 uniform, 1 chord length, 2 centripetal. + #[serde(default)] + parameterization: u32, + }, + Offset { + curve: Crv, + distance: F, + }, + Continuity { + curve: Crv, + parameter: F, + /// `parameter` is relative to the curve's natural interval. + #[serde(default)] + relative: bool, + }, + Compatible { + curves: Vec, + }, + Order { + curves: Vec, + }, +} + +fn a3(p: &P3) -> [f64; 3] { + [p[0].0, p[1].0, p[2].0] +} + +fn v3(p: &P3) -> Vec3 { + Vec3::from_array(a3(p)) +} + +fn in_curve(c: &Crv) -> InCurve { + match c { + Crv::Curve { points } => InCurve::Curve { + points: points.iter().map(a3).collect(), + }, + Crv::Circle { center, radius } => InCurve::Circle { + center: a3(center), + radius: radius.0, + }, + Crv::Arc { + center, + radius, + start_deg, + end_deg, + } => InCurve::Arc { + center: a3(center), + radius: radius.0, + start_deg: start_deg.0, + end_deg: end_deg.0, + }, + Crv::Segment { start, end } => InCurve::Segment { + start: a3(start), + end: a3(end), + }, + Crv::AnalyticCircle { center, radius } => InCurve::AnalyticCircle { + center: a3(center), + radius: radius.0, + }, + Crv::Rectangle { + center, + length, + width, + } => InCurve::Rectangle { + center: a3(center), + length: length.0, + width: width.0, + }, + } +} + +fn to_call(i: &In) -> Call { + match i { + In::Interp { + points, + parameterization, + } => Call::Interp { + points: points.iter().map(a3).collect(), + parameterization: *parameterization, + }, + In::Offset { curve, distance } => Call::Offset { + curve: in_curve(curve), + distance: distance.0, + }, + In::Continuity { + curve, + parameter, + relative, + } => Call::Continuity { + curve: in_curve(curve), + parameter: parameter.0, + relative: *relative, + }, + In::Compatible { curves } => Call::Compatible { + curves: curves.iter().map(in_curve).collect(), + }, + In::Order { curves } => Call::Order { + curves: curves.iter().map(in_curve).collect(), + }, + } +} + +// ---- C++ side --------------------------------------------------------------------- + +fn cpp(i: &In) -> Result { + match ffi::run(&to_call(i)) { + Ok(v) => Ok(Output::new().values("rec", v)), + Err(e) => Err(HarnessError::new(e.to_string())), + } +} + +// ---- Rust side -------------------------------------------------------------------- + +/// The `SmStatus` of a Rust error; `1007` for `InvalidInput`, `-1` for one upstream +/// cannot return. +fn status(e: &SolidError) -> f64 { + match e { + SolidError::InvalidInput { .. } => 1007.0, + SolidError::Upstream { code, .. } => f64::from(*code), + _ => -1.0, + } +} + +fn put_ivl(o: &mut Vec, e: &Extent1d) { + o.extend([e.min(), e.max()]); +} + +/// `put_bspline` of the shim; `type_code` and `natural` are the virtual +/// `GetType` / `GetNaturalInterval` of the object. +fn put_bspline(o: &mut Vec, type_code: i64, c: &BSplineCurve, natural: &Extent1d) { + o.extend([ + type_code as f64, + c.dim() as f64, + f64::from(c.knot_type() as i32), + f64::from(c.form() as i32), + ]); + match c.gw_nurb() { + None => o.push(0.0), + Some(cur) => { + o.extend([1.0, cur.n() as f64, cur.degree() as f64, cur.m() as f64]); + for p in cur.cpts() { + o.extend([p.x, p.y, p.z, p.w]); + } + o.extend_from_slice(cur.knots()); + put_ivl(o, natural); + } + } +} + +/// [`put_bspline`] of a curve of an `SmBSplineCurve` class. +fn put_curve(o: &mut Vec, c: &Curve) -> Result<(), HarnessError> { + let base = match c.kind() { + CurveKind::BSpline(b) => b, + CurveKind::Line(l) => l.base(), + CurveKind::Ellipse(e) => e.base(), + _ => return Err(HarnessError::new("result is not an SmBSplineCurve class")), + }; + put_bspline(o, c.type_code(), base, &c.natural_interval()); + Ok(()) +} + +/// How making an input curve went. +enum Made { + /// Made (appended). + Yes, + /// The constructor failed: the `-2` record. + Failed, + /// The constructor reached an unported unit. + Deferred(Box), +} + +impl Made { + fn of(r: solidcore::Result, push: impl FnOnce(T)) -> Self { + match r { + Ok(v) => { + push(v); + Self::Yes + } + Err(e) => match Output::unported(&e) { + Some(d) => Self::Deferred(Box::new(d)), + None => Self::Failed, + }, + } + } +} + +/// The curves `c` makes, appended to `out`. +fn build(k: &Kernel, c: &Crv, out: &mut Vec) -> Made { + let made: solidcore::Result> = match c { + Crv::Curve { points } => { + let pts: Vec = points.iter().map(v3).collect(); + api_curves::create_curve(&pts).map(|b| vec![Curve::from(b)]) + } + Crv::Circle { center, radius } => { + api_curves::create_circle(v3(center), radius.0).map(|b| vec![Curve::from(b)]) + } + Crv::Arc { + center, + radius, + start_deg, + end_deg, + } => api_curves::create_arc(v3(center), radius.0, start_deg.0, end_deg.0) + .map(|b| vec![Curve::from(b)]), + Crv::Segment { start, end } => { + api_curves::create_line_segment(v3(start), v3(end)).map(|l| vec![Curve::from(l)]) + } + Crv::AnalyticCircle { center, radius } => { + api_curves::create_analytic_circle(k, v3(center), radius.0) + .map(|e| vec![Curve::from(e)]) + } + Crv::Rectangle { + center, + length, + width, + } => api_curves::create_rectangle(v3(center), length.0, width.0) + .map(|v| v.into_iter().map(Curve::from).collect()), + }; + Made::of(made, |v| out.extend(v)) +} + +/// The B-spline curve `c` makes (kinds curve, circle, arc), as [`build`]. +fn build_plain(c: &Crv, out: &mut Vec) -> Made { + let made = match c { + Crv::Curve { points } => { + let pts: Vec = points.iter().map(v3).collect(); + api_curves::create_curve(&pts) + } + Crv::Circle { center, radius } => api_curves::create_circle(v3(center), radius.0), + Crv::Arc { + center, + radius, + start_deg, + end_deg, + } => api_curves::create_arc(v3(center), radius.0, start_deg.0, end_deg.0), + _ => return Made::Failed, + }; + Made::of(made, |b| out.push(b)) +} + +fn record(o: Vec) -> Output { + Output::new().values("rec", o) +} + +fn failed(e: &SolidError) -> Output { + Output::unported(e).unwrap_or_else(|| record(vec![status(e)])) +} + +fn param_method(code: u32) -> Option { + Some(match code { + 0 => ParamMethod::Uniform, + 1 => ParamMethod::ChordLength, + 2 => ParamMethod::Centripetal, + _ => return None, + }) +} + +fn rust(i: &In) -> Result { + let k = Kernel::default(); + // the one curve of an offset or continuity case, else the case's record + let one = |c: &Crv| -> Result>, HarnessError> { + let mut v = Vec::new(); + match build(&k, c, &mut v) { + Made::Deferred(d) => Ok(Err(d)), + Made::Failed => Ok(Err(Box::new(record(vec![-2.0])))), + Made::Yes => v + .into_iter() + .next() + .map(Ok) + .ok_or_else(|| HarnessError::new("no curve built")), + } + }; + Ok(match i { + In::Interp { + points, + parameterization, + } => { + let pts: Vec = points.iter().map(v3).collect(); + let par = param_method(*parameterization) + .ok_or_else(|| HarnessError::new("bad parameterization"))?; + match api::create_curve_interp_points(&pts, par) { + Ok(c) => { + let mut o = vec![0.0]; + put_bspline(&mut o, BSplineCurve::TYPE, &c, &c.natural_interval()); + record(o) + } + Err(e) => failed(&e), + } + } + In::Offset { curve, distance } => { + let c = match one(curve)? { + Ok(c) => c, + Err(out) => return Ok(*out), + }; + match api::offset_curve(&k, &c, distance.0) { + Ok(r) => { + let mut o = vec![0.0]; + put_curve(&mut o, &r)?; + record(o) + } + Err(e) => failed(&e), + } + } + In::Continuity { + curve, + parameter, + relative, + } => { + let c = match one(curve)? { + Ok(c) => c, + Err(out) => return Ok(*out), + }; + let t = if *relative { + c.natural_interval().evaluate(parameter.0) + } else { + parameter.0 + }; + match api::evaluate_continuity(&k, &c, t) { + Ok(ct) => record(vec![0.0, f64::from(ct as i32)]), + Err(e) => failed(&e), + } + } + In::Compatible { curves } => { + let mut v = Vec::new(); + for c in curves { + match build_plain(c, &mut v) { + Made::Deferred(d) => return Ok(*d), + Made::Failed => return Ok(record(vec![-2.0])), + Made::Yes => {} + } + } + match api::make_curves_compatible(&mut v) { + Ok(()) => { + let mut o = vec![0.0, v.len() as f64]; + for c in &v { + put_bspline(&mut o, BSplineCurve::TYPE, c, &c.natural_interval()); + } + record(o) + } + Err(e) => failed(&e), + } + } + In::Order { curves } => { + let mut v = Vec::new(); + for c in curves { + match build(&k, c, &mut v) { + Made::Deferred(d) => return Ok(*d), + Made::Failed => return Ok(record(vec![-2.0])), + Made::Yes => {} + } + } + match api::order_curves(&k, &v) { + Ok(order) => { + let mut o = vec![0.0, order.len() as f64]; + o.extend(order.iter().map(|&i| i as f64)); + record(o) + } + Err(e) => failed(&e), + } + } + }) +} + +// ---- generation ------------------------------------------------------------------- + +fn p3(p: [f64; 3]) -> P3 { + [F(p[0]), F(p[1]), F(p[2])] +} + +/// A "nasty" float: NaN, infinities, zero, negatives, tiny or huge values. +fn nasty(rng: &mut Rng) -> f64 { + let choices = [ + f64::NAN, + f64::INFINITY, + 0.0, + -1.0, + 1.0e-12, + 1.0e-7, + 1.0e12, + ]; + rng.choose(&choices).copied().unwrap_or(0.0) +} + +/// Mostly a moderate value in `[lo, hi]`, sometimes a nasty one. +fn val(rng: &mut Rng, lo: f64, hi: f64) -> f64 { + if rng.chance(0.04) { + nasty(rng) + } else { + rng.uniform(lo, hi) + } +} + +fn pt(rng: &mut Rng) -> P3 { + if rng.chance(0.02) { + let mut p = rng.point3(-50.0, 50.0); + if let Some(c) = p.get_mut(rng.below(3) as usize) { + *c = nasty(rng); + } + p3(p) + } else { + p3(rng.point3(-50.0, 50.0)) + } +} + +/// A planar point (z = `z`) in a box. +fn flat(rng: &mut Rng, z: f64) -> P3 { + p3([rng.uniform(-30.0, 30.0), rng.uniform(-30.0, 30.0), z]) +} + +/// One curve of the kinds up to `max` (0 curve, 1 circle, 2 arc, 3 segment, 4 analytic +/// circle), planar at `z` most of the time. +fn gen_curve(rng: &mut Rng, max: u64) -> Crv { + let z = if rng.chance(0.8) { 0.0 } else { rng.uniform(-5.0, 5.0) }; + match rng.below(max + 1) { + 0 => { + let n = 4 + rng.below(6) as usize; + Crv::Curve { + points: (0..n) + .map(|_| if rng.chance(0.85) { flat(rng, z) } else { pt(rng) }) + .collect(), + } + } + 1 => Crv::Circle { + center: flat(rng, z), + radius: F(val(rng, 0.5, 30.0)), + }, + 2 => Crv::Arc { + center: flat(rng, z), + radius: F(val(rng, 0.5, 30.0)), + start_deg: F(rng.uniform(-180.0, 180.0)), + end_deg: F(rng.uniform(-180.0, 360.0)), + }, + 3 => Crv::Segment { + start: flat(rng, z), + end: if rng.chance(0.85) { flat(rng, z) } else { pt(rng) }, + }, + _ => Crv::AnalyticCircle { + center: flat(rng, z), + radius: F(val(rng, 0.5, 30.0)), + }, + } +} + +/// The segments of a closed polygon through `corners`, shuffled and some reversed. +fn polygon_loop(rng: &mut Rng, corners: &[[f64; 3]]) -> Vec { + let n = corners.len(); + let mut segs: Vec = (0..n) + .map(|i| { + let a = corners.get(i).copied().unwrap_or([0.0; 3]); + let b = corners.get((i + 1) % n).copied().unwrap_or([0.0; 3]); + if rng.chance(0.3) { + Crv::Segment { + start: p3(b), + end: p3(a), + } + } else { + Crv::Segment { + start: p3(a), + end: p3(b), + } + } + }) + .collect(); + // Fisher-Yates with the case's generator + for i in (1..segs.len()).rev() { + let j = rng.below(i as u64 + 1) as usize; + segs.swap(i, j); + } + segs +} + +fn gen_case(rng: &mut Rng) -> In { + match rng.below(5) { + 0 => { + let n = 2 + rng.below(9) as usize; + let mut points: Vec = (0..n).map(|_| pt(rng)).collect(); + if n > 2 && rng.chance(0.1) { + // coincident neighbours + if let Some(&a) = points.first() { + if let Some(b) = points.get_mut(1) { + *b = a; + } + } + } + In::Interp { + points, + parameterization: rng.below(3) as u32, + } + } + 1 => In::Offset { + curve: gen_curve(rng, 4), + distance: F(if rng.chance(0.1) { + nasty(rng) + } else { + rng.uniform(-5.0, 5.0) + }), + }, + 2 => { + let relative = rng.chance(0.9); + let parameter = if relative { + *rng.choose(&[0.0, 0.25, 0.5, 0.75, 1.0, -0.1, 1.1]) + .unwrap_or(&0.5) + } else { + rng.uniform(-1.0, 2.0) + }; + In::Continuity { + curve: gen_curve(rng, 4), + parameter: F(if rng.chance(0.03) { f64::NAN } else { parameter }), + relative, + } + } + 3 => { + let n = rng.below(5) as usize; + In::Compatible { + curves: (0..n).map(|_| gen_curve(rng, 2)).collect(), + } + } + _ => { + let curves = match rng.below(5) { + 0 => vec![Crv::Rectangle { + center: flat(rng, 0.0), + length: F(val(rng, 0.5, 40.0)), + width: F(val(rng, 0.5, 40.0)), + }], + 1 | 2 => { + let n = 3 + rng.below(4) as usize; + let c = [rng.uniform(-10.0, 10.0), rng.uniform(-10.0, 10.0)]; + let r = rng.uniform(1.0, 20.0); + let corners: Vec<[f64; 3]> = (0..n) + .map(|i| { + let a = core::f64::consts::TAU * i as f64 / n as f64; + [c[0] + r * a.cos(), c[1] + r * a.sin(), 0.0] + }) + .collect(); + polygon_loop(rng, &corners) + } + 3 => (0..2 + rng.below(3)) + .map(|_| Crv::Circle { + center: flat(rng, 0.0), + radius: F(rng.uniform(0.5, 10.0)), + }) + .collect(), + _ => (0..rng.below(5)).map(|_| gen_curve(rng, 4)).collect(), + }; + In::Order { curves } + } + } +} + +impl UnitDiff for Runner { + fn id(&self) -> &'static str { + "primitive.api_curve_ops" + } + + 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/primitive_api_curve_ops.rs b/crates/solidcore-ffi/src/units/primitive_api_curve_ops.rs new file mode 100644 index 0000000000..f2a043fa28 --- /dev/null +++ b/crates/solidcore-ffi/src/units/primitive_api_curve_ops.rs @@ -0,0 +1,292 @@ +//! FFI wrappers for unit `primitive.api_curve_ops`. Shim: +//! `harness/cpp/shim/primitive_api_curve_ops.cpp`. Upstream: +//! `source/SM_API/src/SmApiCurves.cpp` (`SmApiCreateCurveInterpPoints`, +//! `SmApiOffsetCurve`, `SmApiEvaluateContinuity`, `SmApiMakeCurvesCompatible`, +//! `SmApiOrderCurves`). +//! +//! Program layout (all `f64`): `op args...`, with `op` the [`Call`] discriminant: +//! +//! | op | args | +//! |----|------| +//! | 1 interpolation | `n pts(3n) parameterization` (`SM_CP_*`, 0 to 2) | +//! | 2 offset | `curve distance` | +//! | 3 continuity | `curve relative parameter` | +//! | 4 make compatible | `count curve...` (kinds 0 to 2) | +//! | 5 order | `count curve...` (kinds 0 to 5) | +//! +//! With `relative = 1` the continuity parameter is the curve's natural interval evaluated +//! there (`SmExtent1d::Evaluate`). +//! +//! A `curve` is `kind args`, made by an `SM_API` constructor ([`InCurve`]): `0 n pts(3n)` +//! (`SmApiCreateCurve`), `1 center(3) radius` (`SmApiCreateCircle`, B-spline), +//! `2 center(3) radius start_deg end_deg` (`SmApiCreateArc`), `3 start(3) end(3)` +//! (`SmApiCreateLineSegment`), `4 center(3) radius` (`SmApiCreateCircle`, `SmCircle`), +//! `5 center(3) length width` (`SmApiCreateRectangle`, its 4 segments). +//! +//! Output: `-2` when an input curve's constructor fails; else the API status (`0` = +//! `SM_SUCCESS`, else the `SmStatus`), then on success: a B-spline `type dim knot_type +//! form has_nurb [n p m Pw(4(n+1)) U(m+1) natural_interval(2)]` (interpolation, offset); +//! the `SmContinuityType` code (continuity); `count` then each curve as a B-spline (make +//! compatible); `count` then each curve's input index (order). + +use crate::validate::bad; +use crate::{call_with_buffer, FfiError}; + +mod sys { + extern "C" { + pub fn sc_primitive_api_curve_ops_run( + input: *const f64, + n_in: i64, + out: *mut f64, + cap: i64, + out_len: *mut i64, + ) -> i32; + } +} + +/// Most points of a curve. +pub const MAX_POINTS: usize = 4096; +/// Most curves of a list. +pub const MAX_CURVES: usize = 64; + +/// One input curve, made by an `SM_API` constructor. +#[derive(Debug, Clone, PartialEq)] +pub enum InCurve { + /// `SmApiCreateCurve(points)`. + Curve { + /// Control points (at most [`MAX_POINTS`]). + points: Vec<[f64; 3]>, + }, + /// `SmApiCreateCircle(center, radius, SmBSplineCurve*&)`. + Circle { + /// Center. + center: [f64; 3], + /// Radius. + radius: f64, + }, + /// `SmApiCreateArc(center, radius, start_deg, end_deg)`. + Arc { + /// Center. + center: [f64; 3], + /// Radius. + radius: f64, + /// Start angle (degrees). + start_deg: f64, + /// End angle (degrees). + end_deg: f64, + }, + /// `SmApiCreateLineSegment(start, end)`. + Segment { + /// Start point. + start: [f64; 3], + /// End point. + end: [f64; 3], + }, + /// `SmApiCreateCircle(center, radius, SmCircle*&)`. + AnalyticCircle { + /// Center. + center: [f64; 3], + /// Radius. + radius: f64, + }, + /// `SmApiCreateRectangle(center, length, width)`: its 4 segments (lists only). + Rectangle { + /// Center. + center: [f64; 3], + /// Length (X). + length: f64, + /// Width (Y). + width: f64, + }, +} + +impl InCurve { + /// The shim's kind code. + const fn kind(&self) -> u32 { + match self { + Self::Curve { .. } => 0, + Self::Circle { .. } => 1, + Self::Arc { .. } => 2, + Self::Segment { .. } => 3, + Self::AnalyticCircle { .. } => 4, + Self::Rectangle { .. } => 5, + } + } +} + +/// One API call. +#[derive(Debug, Clone, PartialEq)] +pub enum Call { + /// `SmApiCreateCurveInterpPoints(points, rpResult, parameterization)`. + Interp { + /// Points (at most [`MAX_POINTS`]). + points: Vec<[f64; 3]>, + /// `SmCurveParameterizationType` code (0 to 2). + parameterization: u32, + }, + /// `SmApiOffsetCurve(curve, distance, rpResult)`. + Offset { + /// The curve (not a rectangle). + curve: InCurve, + /// Offset distance. + distance: f64, + }, + /// `SmApiEvaluateContinuity(curve, parameter, rType)`. + Continuity { + /// The curve (not a rectangle). + curve: InCurve, + /// Parameter (relative to the natural interval with `relative`). + parameter: f64, + /// `parameter` is relative to the curve's natural interval. + relative: bool, + }, + /// `SmApiMakeCurvesCompatible(curves)`. + Compatible { + /// The curves (B-spline kinds: curve, circle, arc). + curves: Vec, + }, + /// `SmApiOrderCurves(curves)`. + Order { + /// The curves (any kind; a rectangle adds its 4 segments). + curves: Vec, + }, +} + +fn put_curve(o: &mut Vec, c: &InCurve, max_kind: u32) -> Result<(), FfiError> { + if c.kind() > max_kind { + return Err(bad("curve kind not allowed here")); + } + o.push(f64::from(c.kind())); + match c { + InCurve::Curve { points } => { + if points.len() > MAX_POINTS { + return Err(bad("too many points")); + } + o.push(points.len() as f64); + o.extend(points.iter().flatten()); + } + InCurve::Circle { center, radius } | InCurve::AnalyticCircle { center, radius } => { + o.extend_from_slice(center); + o.push(*radius); + } + InCurve::Arc { + center, + radius, + start_deg, + end_deg, + } => { + o.extend_from_slice(center); + o.extend([*radius, *start_deg, *end_deg]); + } + InCurve::Segment { start, end } => { + o.extend_from_slice(start); + o.extend_from_slice(end); + } + InCurve::Rectangle { + center, + length, + width, + } => { + o.extend_from_slice(center); + o.extend([*length, *width]); + } + } + Ok(()) +} + +fn put_list(o: &mut Vec, curves: &[InCurve], max_kind: u32) -> Result<(), FfiError> { + if curves.len() > MAX_CURVES { + return Err(bad("too many curves")); + } + o.push(curves.len() as f64); + for c in curves { + put_curve(o, c, max_kind)?; + } + Ok(()) +} + +fn encode(call: &Call) -> Result, FfiError> { + let mut o = Vec::new(); + match call { + Call::Interp { + points, + parameterization, + } => { + if points.len() > MAX_POINTS { + return Err(bad("too many points")); + } + if *parameterization > 2 { + return Err(bad("parameterization")); + } + o.push(1.0); + o.push(points.len() as f64); + o.extend(points.iter().flatten()); + o.push(f64::from(*parameterization)); + } + Call::Offset { curve, distance } => { + o.push(2.0); + put_curve(&mut o, curve, 4)?; + o.push(*distance); + } + Call::Continuity { + curve, + parameter, + relative, + } => { + o.push(3.0); + put_curve(&mut o, curve, 4)?; + o.extend([if *relative { 1.0 } else { 0.0 }, *parameter]); + } + Call::Compatible { curves } => { + o.push(4.0); + put_list(&mut o, curves, 2)?; + } + Call::Order { curves } => { + o.push(5.0); + put_list(&mut o, curves, 5)?; + } + } + Ok(o) +} + +/// Runs one API call; returns the record described in the module docs. +/// +/// # Errors +/// +/// [`FfiError::InvalidArgument`] for input the shim would reject; the shim's status +/// otherwise. +pub fn run(call: &Call) -> Result, FfiError> { + let prog = encode(call)?; + let n_in = i64::try_from(prog.len()).map_err(|_| bad("program too long"))?; + call_with_buffer(256, |buf: &mut [f64], len| { + // SAFETY: `prog` is valid for `n_in` reads; `buf` for `buf.len()` writes; `len` + // is a valid i64; nothing is retained. + unsafe { + sys::sc_primitive_api_curve_ops_run( + prog.as_ptr(), + n_in, + buf.as_mut_ptr(), + buf.len() as i64, + &raw mut *len, + ) + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_rectangle_is_refused_where_one_curve_is_expected() { + let call = Call::Offset { + curve: InCurve::Rectangle { + center: [0.0; 3], + length: 1.0, + width: 1.0, + }, + distance: 1.0, + }; + assert!(encode(&call).is_err()); + } +} diff --git a/crates/solidcore/src/primitive.rs b/crates/solidcore/src/primitive.rs index e8fe8e3186..6986860364 100644 --- a/crates/solidcore/src/primitive.rs +++ b/crates/solidcore/src/primitive.rs @@ -4,6 +4,7 @@ //! Upstream: `SmPrimitiveCreation` in `source/SMLib`, and //! `SM_API/SmApiPrimitives`. +pub mod api_curve_ops; pub mod api_curves; pub mod api_primitives; pub mod primitive_creation; diff --git a/crates/solidcore/src/primitive/api_curve_ops.rs b/crates/solidcore/src/primitive/api_curve_ops.rs new file mode 100644 index 0000000000..6e4012b364 --- /dev/null +++ b/crates/solidcore/src/primitive/api_curve_ops.rs @@ -0,0 +1,285 @@ +//! `SM_API` curve utilities: interpolation through points, simple offsets, continuity +//! at a parameter, making B-spline curves compatible and ordering curves into loops. +//! +//! Upstream: `source/SM_API/src/SmApiCurves.cpp`, `source/SM_API/inc/SmApiCurves.h`. +//! +//! | Rust | Upstream | +//! |--------------------------------|-----------------------------------| +//! | [`create_curve_interp_points`] | `SmApiCreateCurveInterpPoints` | +//! | [`offset_curve`] | `SmApiOffsetCurve` | +//! | [`evaluate_continuity`] | `SmApiEvaluateContinuity` | +//! | [`make_curves_compatible`] | `SmApiMakeCurvesCompatible` | +//! | [`order_curves`] | `SmApiOrderCurves` | +//! +//! Deviations: +//! - **Out-params.** Results are return values. [`order_curves`] returns the new order +//! as indices into the input instead of reordering the caller's array in place (the +//! caller applies it); on failure there is none, as upstream leaves the array alone. +//! - **Context.** `SmApiGetOrCreateContext()` is the caller's [`Kernel`]. +//! - **Parameterization.** `SmCurveParameterizationType` is [`ParamMethod`]: upstream's +//! check for a value outside `SM_CP_UNIFORM .. SM_CP_CENTRIPETAL` cannot fail here. +//! - **Curve types.** Upstream's `SmBSplineCurve*` arguments reach the virtual +//! `CreateSimpleOffset` of the object's class: [`offset_curve`] takes a [`Curve`] and +//! dispatches to the B-spline, line (`SmLine`) and ellipse / circle (`SmEllipse`, +//! `SmCircle`) overrides; another curve kind (not an `SmBSplineCurve` upstream) is +//! `InvalidInput`. [`make_curves_compatible`] takes plain [`BSplineCurve`]s. +//! - **`GetEnds`.** [`offset_curve`] returns the error of the curve's end points (a +//! curve without NURB), where upstream ignores the status and reads uninitialised +//! points. +//! - **Statuses.** `SM_ERR_INVALID_INPUT` is [`SolidError::InvalidInput`]; the called +//! kernel functions' errors pass through unchanged. + +use crate::curve::curve::p2::evaluate_continuity as curve_continuity; +use crate::curve::curve::p3::compute_crvs_normal; +use crate::curve::curve::{Curve, CurveKind}; +use crate::error::{ignored, Result, SolidError}; +use crate::geom::geom_utility::p2::ContinuityType; +use crate::geom::tol::ZONE_TOL_3D; +use crate::geom::vector3d::{Point3, Vec3}; +use crate::kernel::Kernel; +use crate::math::{EFF_ZERO, EFF_ZERO_SQ}; +use crate::nurbs::b_spline_curve::BSplineCurve; +use crate::nurbs::crv_fit::p1::ParamMethod; +use crate::topology::trimming_tools::order_curves_into_loops; + +const OP_INTERP: &str = "primitive::api_curve_ops::create_curve_interp_points"; +const OP_OFFSET: &str = "primitive::api_curve_ops::offset_curve"; +const OP_CONTINUITY: &str = "primitive::api_curve_ops::evaluate_continuity"; +const OP_COMPATIBLE: &str = "primitive::api_curve_ops::make_curves_compatible"; +const OP_ORDER: &str = "primitive::api_curve_ops::order_curves"; + +/// `SM_CONTINUITY_ANGLE` (degrees): the default G1 angle of `SmCurve::EvaluateContinuity`. +const CONTINUITY_ANGLE: f64 = 0.5; + +/// `SmBSplineCurve::MakeCurvesCompatible`'s default knot tolerance. +const COMPATIBLE_KNOT_TOL: f64 = 1.0e-8; + +/// The cubic B-spline interpolating `points` (at least 4, all defined) with knots by +/// `parameterization` (`SmApiCreateCurveInterpPoints`, through +/// `SmBSplineCurve::CreateInterpolatingCurve` with dimension 3, degree 3 and no +/// vectors). The chord-length and centripetal parameterizations need consecutive +/// points farther apart than `SM_EFF_ZERO`. +/// +/// # Errors +/// +/// `InvalidInput` for fewer than 4 points, an undefined point, or coincident +/// consecutive points with a distance-based parameterization; those of +/// [`BSplineCurve::create_interpolating_curve`]. +pub fn create_curve_interp_points( + points: &[Point3], + parameterization: ParamMethod, +) -> Result { + if points.len() < 4 { + return Err(SolidError::invalid_input(OP_INTERP, "fewer than 4 points")); + } + if points.iter().any(|p| p.is_undef()) { + return Err(SolidError::invalid_input(OP_INTERP, "undefined point")); + } + if matches!( + parameterization, + ParamMethod::ChordLength | ParamMethod::Centripetal + ) && points.windows(2).any(|w| match w { + [a, b] => b.distance_squared(*a) <= EFF_ZERO_SQ, + _ => false, + }) { + return Err(SolidError::invalid_input(OP_INTERP, "coincident points")); + } + BSplineCurve::create_interpolating_curve(parameterization, 3, 3, points, &[], None, false) +} + +/// The offset of `curve` by `distance` (positive to the right of the curve) in the +/// plane of the curve (`SmApiOffsetCurve`): the normal is `SmCurve::ComputeCrvsNormal`'s; +/// when that fails, `(0, 0, 1)`, or `(0, 1, 0)` when the chord between the end points is +/// parallel to it. The offset is the curve class's `CreateSimpleOffset` with +/// approximation tolerance `0.001`. +/// +/// # Errors +/// +/// `InvalidInput` for a curve that is not a B-spline, line, ellipse or circle; the +/// errors of the end points and of the class's `CreateSimpleOffset`. +pub fn offset_curve(k: &Kernel, curve: &Curve, distance: f64) -> Result { + let approx_tol = 0.001; + + // (the curve must be an SmBSplineCurve: checked before any work) + let base = match curve.kind() { + CurveKind::BSpline(c) => c, + CurveKind::Line(l) => l.base(), + CurveKind::Ellipse(e) => e.base(), + _ => return Err(SolidError::invalid_input(OP_OFFSET, "not a B-spline curve")), + }; + + let normal = if let Some(n) = ignored(compute_crvs_normal(k, core::slice::from_ref(curve)))? { + n + } else { + let mut n = Vec3::new(0.0, 0.0, 1.0); + // (the class's virtual GetEnds) + let ends = curve.ends(k)?; + let dir = ends.end - ends.start; + if let Ok(dir) = dir.unitize() { + if dir.cross(n).length() < EFF_ZERO { + n = Vec3::new(0.0, 1.0, 0.0); + } + } + n + }; + + match curve.kind() { + CurveKind::Line(l) => l + .create_simple_offset(normal, distance) + .map(|(c, _)| Curve::from(c)), + CurveKind::Ellipse(e) => e + .create_simple_offset(k, approx_tol, normal, distance, false) + .map(|(c, _)| c), + _ => base + .create_simple_offset(k, approx_tol, normal, distance, false) + .map(|(c, _)| Curve::from(c)), + } +} + +/// The continuity of `curve` at `parameter` between its evaluations from the left and +/// from the right (`SmApiEvaluateContinuity`, through `SmCurve::EvaluateContinuity` +/// with the default G1 angle). +/// +/// # Errors +/// +/// `InvalidInput` for a parameter outside the curve's natural interval (or NaN); those +/// of [`crate::curve::curve::p2::evaluate_continuity`]. +pub fn evaluate_continuity(k: &Kernel, curve: &Curve, parameter: f64) -> Result { + let domain = curve.natural_interval(); + // (a NaN parameter is outside) + if !(domain.min()..=domain.max()).contains(¶meter) { + return Err(SolidError::invalid_input( + OP_CONTINUITY, + "parameter outside the curve's interval", + )); + } + Ok(curve_continuity( + k, + curve, + parameter, + true, + curve, + parameter, + false, + CONTINUITY_ANGLE, + )? + .continuity) +} + +/// Makes the B-spline curves `curves` compatible in place (the same degree and knot +/// vector; `SmApiMakeCurvesCompatible`, through `SmBSplineCurve::MakeCurvesCompatible` +/// with its default knot tolerance `1e-8`). The curves are updated one at a time, so a +/// failure can leave some already modified. +/// +/// # Errors +/// +/// `InvalidInput` for fewer than 2 curves; those of +/// [`BSplineCurve::make_curves_compatible`]. +pub fn make_curves_compatible(curves: &mut [BSplineCurve]) -> Result<()> { + if curves.len() < 2 { + return Err(SolidError::invalid_input( + OP_COMPATIBLE, + "fewer than 2 curves", + )); + } + BSplineCurve::make_curves_compatible(curves, COMPATIBLE_KNOT_TOL) +} + +/// The order of `curves` in loop sequence, as indices into `curves` +/// (`SmApiOrderCurves`, through `SmTrimmingTools::OrderCurvesIntoLoops` with tolerance +/// `SM_ZONE_TOL_3D`). Curves are not reversed and the loop boundaries are not +/// reported, so consecutive curves need not meet head to tail. +/// +/// # Errors +/// +/// `InvalidInput` for fewer than 2 curves; `SM_ERR` when the curves do not form closed +/// planar loops or none are ordered; those of +/// [`order_curves_into_loops`]. +pub fn order_curves(k: &Kernel, curves: &[Curve]) -> Result> { + if curves.len() < 2 { + return Err(SolidError::invalid_input(OP_ORDER, "fewer than 2 curves")); + } + let loops = order_curves_into_loops(k, curves, ZONE_TOL_3D)?; + loops.status()?; + if loops.ordered.is_empty() { + return Err(SolidError::sm_err(OP_ORDER)); + } + Ok(loops.ordered) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::primitive::api_curves::{create_circle, create_curve, create_rectangle}; + + fn p(x: f64, y: f64, z: f64) -> Point3 { + Point3::new(x, y, z) + } + + #[test] + fn interp_checks_inputs() { + let pts = [p(0.0, 0.0, 0.0), p(1.0, 1.0, 0.0), p(2.0, 0.0, 0.0)]; + assert!(matches!( + create_curve_interp_points(&pts, ParamMethod::Uniform), + Err(SolidError::InvalidInput { .. }) + )); + let dup = [ + p(0.0, 0.0, 0.0), + p(0.0, 0.0, 0.0), + p(1.0, 1.0, 0.0), + p(2.0, 0.0, 0.0), + ]; + assert!(matches!( + create_curve_interp_points(&dup, ParamMethod::ChordLength), + Err(SolidError::InvalidInput { .. }) + )); + let ok = [ + p(0.0, 0.0, 0.0), + p(1.0, 1.0, 0.0), + p(2.0, 0.0, 0.0), + p(3.0, 1.0, 0.0), + ]; + assert!(create_curve_interp_points(&ok, ParamMethod::Centripetal).is_ok()); + } + + #[test] + fn offset_of_a_straight_curve_stays_in_its_plane() { + let k = Kernel::default(); + let pts: Vec = (0..4).map(|i| p(f64::from(i) * 10.0, 0.0, 0.0)).collect(); + let line = Curve::from(create_curve(&pts).unwrap()); + let off = offset_curve(&k, &line, 2.0).unwrap(); + let interval = off.natural_interval(); + let start = off.evaluate_point(&k, interval.min()).unwrap(); + assert!(start.z.abs() < 1.0e-9); + assert!((start.y.abs() - 2.0).abs() < 1.0e-6); + } + + #[test] + fn continuity_rejects_outside_parameters() { + let k = Kernel::default(); + let c = Curve::from(create_circle(p(0.0, 0.0, 0.0), 5.0).unwrap()); + let ivl = c.natural_interval(); + assert!(evaluate_continuity(&k, &c, ivl.max() + 1.0).is_err()); + assert!(evaluate_continuity(&k, &c, f64::NAN).is_err()); + assert!(evaluate_continuity(&k, &c, ivl.evaluate(0.5)).is_ok()); + } + + #[test] + fn compatible_and_order_need_two_curves() { + let k = Kernel::default(); + assert!(make_curves_compatible(&mut []).is_err()); + let mut circles = vec![ + create_circle(p(0.0, 0.0, 0.0), 5.0).unwrap(), + create_circle(p(0.0, 0.0, 5.0), 3.0).unwrap(), + ]; + assert!(make_curves_compatible(&mut circles).is_ok()); + assert!(order_curves(&k, &[]).is_err()); + let rect: Vec = create_rectangle(p(0.0, 0.0, 0.0), 10.0, 6.0) + .unwrap() + .into_iter() + .map(Curve::from) + .collect(); + let order = order_curves(&k, &rect).unwrap(); + assert_eq!(order.len(), 4); + } +} diff --git a/fixtures/primitive.api_curve_ops/cases.json b/fixtures/primitive.api_curve_ops/cases.json new file mode 100644 index 0000000000..a3f699d544 --- /dev/null +++ b/fixtures/primitive.api_curve_ops/cases.json @@ -0,0 +1,1066 @@ +{ + "cases": [ + { + "name": "offset_test_curve", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:273", + "inputs": { + "op": "offset", + "curve": { + "kind": "curve", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 10, + -3, + 0 + ], + [ + 20, + 3, + 0 + ], + [ + 30, + -6, + 0 + ] + ] + }, + "distance": 3.0 + } + }, + { + "name": "offset_test_line_xy", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:284", + "inputs": { + "op": "offset", + "curve": { + "kind": "curve", + "points": [ + [ + 0.0, + 0, + 0 + ], + [ + 10.0, + 0, + 0 + ], + [ + 20.0, + 0, + 0 + ], + [ + 30.0, + 0, + 0 + ] + ] + }, + "distance": 2.0 + } + }, + { + "name": "offset_test_line_z", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:303", + "inputs": { + "op": "offset", + "curve": { + "kind": "curve", + "points": [ + [ + 0, + 0, + 0.0 + ], + [ + 0, + 0, + 10.0 + ], + [ + 0, + 0, + 20.0 + ], + [ + 0, + 0, + 30.0 + ] + ] + }, + "distance": 2.0 + } + }, + { + "name": "offset_segment", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:979", + "inputs": { + "op": "offset", + "curve": { + "kind": "segment", + "start": [ + 0, + 0, + 0 + ], + "end": [ + 10, + 0, + 0 + ] + }, + "distance": 1.5 + } + }, + { + "name": "offset_circle_out", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:979", + "inputs": { + "op": "offset", + "curve": { + "kind": "circle", + "center": [ + 1, + 2, + 0 + ], + "radius": 5.0 + }, + "distance": 2.0 + } + }, + { + "name": "offset_circle_in", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:979", + "inputs": { + "op": "offset", + "curve": { + "kind": "circle", + "center": [ + 1, + 2, + 0 + ], + "radius": 5.0 + }, + "distance": -2.0 + } + }, + { + "name": "offset_analytic_circle", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:979", + "inputs": { + "op": "offset", + "curve": { + "kind": "analytic_circle", + "center": [ + 0, + 0, + 0 + ], + "radius": 4.0 + }, + "distance": 1.0 + } + }, + { + "name": "offset_arc", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:979", + "inputs": { + "op": "offset", + "curve": { + "kind": "arc", + "center": [ + 0, + 0, + 0 + ], + "radius": 10.0, + "start_deg": 0.0, + "end_deg": 90.0 + }, + "distance": -3.0 + } + }, + { + "name": "interp_too_few", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:993", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 2, + 0, + 0 + ] + ], + "parameterization": 0 + } + }, + { + "name": "interp_nan_point", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1005", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + "NaN", + 0, + 0 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 2 + } + }, + { + "name": "interp_uniform", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:908", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 2, + 0, + 0 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 0 + } + }, + { + "name": "interp_six_uniform", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:908", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 2, + 3, + 1 + ], + [ + 5, + 4, + 0 + ], + [ + 7, + 1, + -1 + ], + [ + 9, + -2, + 0 + ], + [ + 12, + 0, + 2 + ] + ], + "parameterization": 0 + } + }, + { + "name": "interp_chordlength", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:908", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 2, + 0, + 0 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 1 + } + }, + { + "name": "interp_six_chordlength", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:908", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 2, + 3, + 1 + ], + [ + 5, + 4, + 0 + ], + [ + 7, + 1, + -1 + ], + [ + 9, + -2, + 0 + ], + [ + 12, + 0, + 2 + ] + ], + "parameterization": 1 + } + }, + { + "name": "interp_centripetal", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:908", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 2, + 0, + 0 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 2 + } + }, + { + "name": "interp_six_centripetal", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:908", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 2, + 3, + 1 + ], + [ + 5, + 4, + 0 + ], + [ + 7, + 1, + -1 + ], + [ + 9, + -2, + 0 + ], + [ + 12, + 0, + 2 + ] + ], + "parameterization": 2 + } + }, + { + "name": "interp_dup_chordlength", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1072", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 1 + } + }, + { + "name": "interp_near_dup_chordlength", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1072", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 1, + 1, + 5e-13 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 1 + } + }, + { + "name": "interp_all_same_chordlength", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1072", + "inputs": { + "op": "interp", + "points": [ + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ] + ], + "parameterization": 1 + } + }, + { + "name": "interp_dup_centripetal", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1072", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 2 + } + }, + { + "name": "interp_near_dup_centripetal", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1072", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 1, + 1, + 5e-13 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 2 + } + }, + { + "name": "interp_all_same_centripetal", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1072", + "inputs": { + "op": "interp", + "points": [ + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ] + ], + "parameterization": 2 + } + }, + { + "name": "interp_dup_uniform", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1072", + "inputs": { + "op": "interp", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 1, + 1, + 0 + ], + [ + 3, + 1, + 0 + ] + ], + "parameterization": 0 + } + }, + { + "name": "continuity_segment_mid", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1097", + "inputs": { + "op": "continuity", + "curve": { + "kind": "segment", + "start": [ + 0, + 0, + 0 + ], + "end": [ + 10, + 0, + 0 + ] + }, + "parameter": 0.5 + } + }, + { + "name": "continuity_segment_nan", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1098", + "inputs": { + "op": "continuity", + "curve": { + "kind": "segment", + "start": [ + 0, + 0, + 0 + ], + "end": [ + 10, + 0, + 0 + ] + }, + "parameter": "NaN" + } + }, + { + "name": "continuity_segment_past", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1100", + "inputs": { + "op": "continuity", + "curve": { + "kind": "segment", + "start": [ + 0, + 0, + 0 + ], + "end": [ + 10, + 0, + 0 + ] + }, + "parameter": 2.0, + "relative": true + } + }, + { + "name": "continuity_curve_knot", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1083", + "inputs": { + "op": "continuity", + "curve": { + "kind": "curve", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 10, + -3, + 0 + ], + [ + 20, + 3, + 0 + ], + [ + 30, + -6, + 0 + ], + [ + 40, + 0, + 0 + ], + [ + 50, + 5, + 0 + ] + ] + }, + "parameter": 0.5, + "relative": true + } + }, + { + "name": "continuity_circle_seam", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1083", + "inputs": { + "op": "continuity", + "curve": { + "kind": "circle", + "center": [ + 0, + 0, + 0 + ], + "radius": 5.0 + }, + "parameter": 0.25, + "relative": true + } + }, + { + "name": "continuity_analytic_circle", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1083", + "inputs": { + "op": "continuity", + "curve": { + "kind": "analytic_circle", + "center": [ + 0, + 0, + 0 + ], + "radius": 5.0 + }, + "parameter": 0.5, + "relative": true + } + }, + { + "name": "compatible_two_circles", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:683", + "inputs": { + "op": "compatible", + "curves": [ + { + "kind": "circle", + "center": [ + 0, + 0, + 0 + ], + "radius": 5.0 + }, + { + "kind": "circle", + "center": [ + 0, + 0, + 5 + ], + "radius": 3.0 + } + ] + } + }, + { + "name": "compatible_none", + "upstream_ref": "tests/SM_API_test/src/TestSmErrorCodes.cpp:247", + "inputs": { + "op": "compatible", + "curves": [] + } + }, + { + "name": "compatible_one", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1302", + "inputs": { + "op": "compatible", + "curves": [ + { + "kind": "circle", + "center": [ + 0, + 0, + 0 + ], + "radius": 5.0 + } + ] + } + }, + { + "name": "compatible_curve_and_arc", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1302", + "inputs": { + "op": "compatible", + "curves": [ + { + "kind": "curve", + "points": [ + [ + 0, + 0, + 0 + ], + [ + 10, + -3, + 0 + ], + [ + 20, + 3, + 0 + ], + [ + 30, + -6, + 0 + ], + [ + 40, + 0, + 0 + ] + ] + }, + { + "kind": "arc", + "center": [ + 0, + 0, + 0 + ], + "radius": 10.0, + "start_deg": 0.0, + "end_deg": 120.0 + } + ] + } + }, + { + "name": "order_rectangle", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:700", + "inputs": { + "op": "order", + "curves": [ + { + "kind": "rectangle", + "center": [ + 0, + 0, + 0 + ], + "length": 10.0, + "width": 6.0 + } + ] + } + }, + { + "name": "order_none", + "upstream_ref": "tests/SM_API_test/src/TestSmErrorCodes.cpp:248", + "inputs": { + "op": "order", + "curves": [] + } + }, + { + "name": "order_shuffled_square", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1323", + "inputs": { + "op": "order", + "curves": [ + { + "kind": "segment", + "start": [ + 10, + 0, + 0 + ], + "end": [ + 10, + 10, + 0 + ] + }, + { + "kind": "segment", + "start": [ + 0, + 10, + 0 + ], + "end": [ + 0, + 0, + 0 + ] + }, + { + "kind": "segment", + "start": [ + 0, + 0, + 0 + ], + "end": [ + 10, + 0, + 0 + ] + }, + { + "kind": "segment", + "start": [ + 10, + 10, + 0 + ], + "end": [ + 0, + 10, + 0 + ] + } + ] + } + }, + { + "name": "order_open_chain", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1323", + "inputs": { + "op": "order", + "curves": [ + { + "kind": "segment", + "start": [ + 0, + 0, + 0 + ], + "end": [ + 10, + 0, + 0 + ] + }, + { + "kind": "segment", + "start": [ + 10, + 0, + 0 + ], + "end": [ + 10, + 10, + 0 + ] + } + ] + } + }, + { + "name": "order_two_circles", + "upstream_ref": "tests/SM_API_test/src/TestSmCurves.cpp:1323", + "inputs": { + "op": "order", + "curves": [ + { + "kind": "circle", + "center": [ + 0, + 0, + 0 + ], + "radius": 5.0 + }, + { + "kind": "circle", + "center": [ + 20, + 0, + 0 + ], + "radius": 3.0 + } + ] + } + } + ] +} \ No newline at end of file diff --git a/harness/cpp/shim/primitive_api_curve_ops.cpp b/harness/cpp/shim/primitive_api_curve_ops.cpp new file mode 100644 index 0000000000..1230472cc4 --- /dev/null +++ b/harness/cpp/shim/primitive_api_curve_ops.cpp @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: Apache-2.0 +// difftest shim for unit `primitive.api_curve_ops`: the SM_API curve utilities of +// source/SM_API/src/SmApiCurves.cpp (SmApiCreateCurveInterpPoints, SmApiOffsetCurve, +// SmApiEvaluateContinuity, SmApiMakeCurvesCompatible, SmApiOrderCurves), called exactly +// as a caller of the API would, on curves made by the SM_API curve constructors. +// +// One entry point, sc_primitive_api_curve_ops_run(in, n_in, out, cap, out_len). `in` is +// `op args...`; layouts are documented in crates/solidcore-ffi/src/units/primitive_api_curve_ops.rs. +// Every read is bounds checked here (a bad program is SC_ERR_ARG). `out` receives `-2` when an +// input curve's constructor fails, else the API status (0 = SM_SUCCESS) and, on success, the +// result. Private members are read through explicit-instantiation access. +#include "sc_shim.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "sc_sm.h" + +using sc::put_ivl; +using sc::st; + +namespace +{ +// Ops (keep in sync with crates/solidcore-ffi/src/units/primitive_api_curve_ops.rs). +enum : int32_t +{ + OP_INTERP = 1, + OP_OFFSET = 2, + OP_CONTINUITY = 3, + OP_COMPATIBLE = 4, + OP_ORDER = 5, +}; + +// Input curve kinds. +enum : int32_t +{ + K_CURVE = 0, // SmApiCreateCurve(points) + K_CIRCLE = 1, // SmApiCreateCircle(center, radius, SmBSplineCurve*&) + K_ARC = 2, // SmApiCreateArc(center, radius, start_deg, end_deg) + K_SEGMENT = 3, // SmApiCreateLineSegment(start, end) + K_ANALYTIC = 4, // SmApiCreateCircle(center, radius, SmCircle*&) + K_RECTANGLE = 5, // SmApiCreateRectangle(center, length, width): 4 segments (lists only) +}; + +// Access to private / protected members through explicit instantiation. +template +struct Rob +{ + friend typename Tag::type get(Tag) { return M; } +}; + +struct BsForm +{ + typedef SmBSplineCurveForm SmBSplineCurve::*type; + friend type get(BsForm); +}; +template struct Rob; + +struct BsKnotType +{ + typedef SmKnotType SmBSplineCurve::*type; + friend type get(BsKnotType); +}; +template struct Rob; + +struct Bad : std::runtime_error +{ + Bad() : std::runtime_error("bad program") {} +}; + +// An input curve's constructor failed: the case compares only this marker. +struct BuildFailed +{ +}; + +struct Rd +{ + const double* in; + int64_t n; + int64_t at; + double num() + { + if (at >= n) + throw Bad(); + return in[at++]; + } + int64_t idx(int64_t lo, int64_t hi) + { + double d = num(); + if (!(d >= (double)lo && d <= (double)hi) || d != (double)(int64_t)d) + throw Bad(); + return (int64_t)d; + } + SmVector3d vec() + { + double x = num(); + double y = num(); + double z = num(); + return SmVector3d(x, y, z); + } +}; + +// The curves a case owns. +struct Owned +{ + SmTArray curves; + ~Owned() + { + for (ULONG i = 0; i < curves.GetSize(); i++) + delete curves[i]; + } +}; + +// type dim knot_type form, the NURB (`has n p m Pw U`), natural interval. +void put_bspline(std::vector& o, SmBSplineCurve* c) +{ + o.push_back((double)c->GetType()); + o.push_back((double)c->GetDim()); + o.push_back((double)(c->*get(BsKnotType()))); + o.push_back((double)(c->*get(BsForm()))); + const gw_CURVE* cur = c->GetGwNurbPointer(); + o.push_back(cur ? 1.0 : 0.0); + if (cur) + { + gw_INDEX n = cur->pol->n; + gw_INDEX m = cur->knt->m; + o.push_back((double)n); + o.push_back((double)cur->p); + o.push_back((double)m); + for (gw_INDEX i = 0; i <= n; i++) + { + o.push_back(cur->pol->Pw[i].x); + o.push_back(cur->pol->Pw[i].y); + o.push_back(cur->pol->Pw[i].z); + o.push_back(cur->pol->Pw[i].w); + } + for (gw_INDEX i = 0; i <= m; i++) + o.push_back(cur->knt->U[i]); + put_ivl(o, c->GetNaturalInterval()); + } +} + +// Reads one curve spec of a kind in [0, max_kind] and appends what it makes to `own`. +void read_curve(Rd& rd, int64_t max_kind, Owned& own) +{ + int64_t kind = rd.idx(K_CURVE, max_kind); + SmStatus s = SM_SUCCESS; + switch (kind) + { + case K_CURVE: + { + int64_t np = rd.idx(0, 4096); + SmTArray pts; + for (int64_t i = 0; i < np; i++) + pts.Add(rd.vec()); + SmBSplineCurve* c = NULL; + s = SmApiCreateCurve(pts, c); + if (c) + own.curves.Add(c); + break; + } + case K_CIRCLE: + { + SmPoint3d c0 = rd.vec(); + double r = rd.num(); + SmBSplineCurve* c = NULL; + s = SmApiCreateCircle(c0, r, c); + if (c) + own.curves.Add(c); + break; + } + case K_ARC: + { + SmPoint3d c0 = rd.vec(); + double r = rd.num(); + double a0 = rd.num(); + double a1 = rd.num(); + SmBSplineCurve* c = NULL; + s = SmApiCreateArc(c0, r, a0, a1, c); + if (c) + own.curves.Add(c); + break; + } + case K_SEGMENT: + { + SmPoint3d a = rd.vec(); + SmPoint3d b = rd.vec(); + SmLine* l = NULL; + s = SmApiCreateLineSegment(a, b, l); + if (l) + own.curves.Add(l); + break; + } + case K_ANALYTIC: + { + SmPoint3d c0 = rd.vec(); + double r = rd.num(); + SmCircle* c = NULL; + s = SmApiCreateCircle(c0, r, c); + if (c) + own.curves.Add(c); + break; + } + default: + { + // K_RECTANGLE + SmPoint3d c0 = rd.vec(); + double len = rd.num(); + double wid = rd.num(); + SmTArray segs; + s = SmApiCreateRectangle(c0, len, wid, segs); + for (ULONG i = 0; i < segs.GetSize(); i++) + own.curves.Add(segs[i]); + break; + } + } + if (s != SM_SUCCESS) + throw BuildFailed(); +} + +void read_list(Rd& rd, int64_t max_kind, Owned& own) +{ + int64_t count = rd.idx(0, 64); + for (int64_t i = 0; i < count; i++) + read_curve(rd, max_kind, own); +} + +void run(Rd& rd, std::vector& o) +{ + int64_t op = rd.idx(OP_INTERP, OP_ORDER); + switch (op) + { + case OP_INTERP: + { + // n pts(3n) parameterization + int64_t np = rd.idx(0, 4096); + SmTArray pts; + for (int64_t i = 0; i < np; i++) + pts.Add(rd.vec()); + int64_t par = rd.idx(SM_CP_UNIFORM, SM_CP_CENTRIPETAL); + SmCurve* res = NULL; + SmStatus s = SmApiCreateCurveInterpPoints(pts, res, (SmCurveParameterizationType)par); + o.push_back(st(s)); + if (s == SM_SUCCESS && res) + put_bspline(o, (SmBSplineCurve*)res); + delete res; + return; + } + case OP_OFFSET: + { + // curve distance + Owned own; + read_curve(rd, K_ANALYTIC, own); + double d = rd.num(); + SmBSplineCurve* res = NULL; + SmStatus s = SmApiOffsetCurve(own.curves[0], d, res); + o.push_back(st(s)); + if (s == SM_SUCCESS && res) + put_bspline(o, res); + delete res; + return; + } + case OP_CONTINUITY: + { + // curve relative parameter + Owned own; + read_curve(rd, K_ANALYTIC, own); + bool relative = rd.idx(0, 1) != 0; + double t = rd.num(); + if (relative) + t = own.curves[0]->GetNaturalInterval().Evaluate(t); + SmContinuityType ct = SM_CT_UNDEFINED; + SmStatus s = SmApiEvaluateContinuity(own.curves[0], t, ct); + o.push_back(st(s)); + if (s == SM_SUCCESS) + o.push_back((double)ct); + return; + } + case OP_COMPATIBLE: + { + // count curves (plain B-spline kinds) + Owned own; + read_list(rd, K_ARC, own); + SmStatus s = SmApiMakeCurvesCompatible(own.curves); + o.push_back(st(s)); + if (s == SM_SUCCESS) + { + o.push_back((double)own.curves.GetSize()); + for (ULONG i = 0; i < own.curves.GetSize(); i++) + put_bspline(o, own.curves[i]); + } + return; + } + default: + { + // OP_ORDER: count curves + Owned own; + read_list(rd, K_RECTANGLE, own); + SmTArray work; + for (ULONG i = 0; i < own.curves.GetSize(); i++) + work.Add(own.curves[i]); + SmStatus s = SmApiOrderCurves(work); + o.push_back(st(s)); + if (s == SM_SUCCESS) + { + o.push_back((double)work.GetSize()); + for (ULONG i = 0; i < work.GetSize(); i++) + { + ULONG at = 0; + if (!own.curves.FindElement(work[i], at)) + throw Bad(); + o.push_back((double)at); + } + } + return; + } + } +} +} // namespace + +SC_EXPORT int32_t sc_primitive_api_curve_ops_run(const double* in, int64_t n_in, double* out, + int64_t cap, int64_t* out_len) +{ + SC_TRY + Rd rd{in, n_in, 0}; + std::vector o; + try + { + run(rd, o); + } + catch (const Bad&) + { + return SC_ERR_ARG; + } + catch (const BuildFailed&) + { + o.assign(1, -2.0); + return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len); + } + if (rd.at != rd.n) + return SC_ERR_ARG; + return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len); + SC_CATCH +} diff --git a/plan/units/primitive.api_curve_ops.toml b/plan/units/primitive.api_curve_ops.toml new file mode 100644 index 0000000000..2a84659626 --- /dev/null +++ b/plan/units/primitive.api_curve_ops.toml @@ -0,0 +1,12 @@ +id = "primitive.api_curve_ops" +upstream = ["source/SM_API/src/SmApiCurves.cpp"] +rust_module = "primitive::api_curve_ops" +depends_on = ["curve.curve_p2", "curve.curve_p3", "nurbs.b_spline_curve_n_lib", "nurbs.b_spline_curve_p2", "primitive.api_curves", "topology.trimming_tools"] +soft_depends_on = ["curve.ellipse_p1", "curve.line", "geom.tol"] +est_lines = 190 +layer = 42 +tests = ["SM_API_test:TestSmOffsetCurve", "SM_API_test:TestSmMakeCurvesCompatible", "SM_API_test:TestSmOrderCurves", "SM_API_test:TestSmErrorCodes"] +status = "green" +claimed_by = "fixer-3 (PR #1538)" +claimed_at = "2026-10-29T00:00:00Z" +notes = "GREEN: difftest 638 cases, 638 pass, 0 failed, 0 deferred (local integration run on bb06099f1). NOT IN callgraph/units.json: wrapper-only SM_API functions of SmApiCurves.cpp (1.0.1 parity batch 3, curves): SmApiCreateCurveInterpPoints, SmApiOffsetCurve, SmApiEvaluateContinuity, SmApiMakeCurvesCompatible, SmApiOrderCurves; every callee is ported (CreateInterpolatingCurve, ComputeCrvsNormal, CreateSimpleOffset of SmBSplineCurve/SmLine/SmEllipse, SmCurve::EvaluateContinuity, MakeCurvesCompatible, OrderCurvesIntoLoops). Input curves are made by the SM_API constructors of primitive.api_curves on both sides. Status set by the gate."