Merge pull request #1538 from tangency-labs/port/smapi-curves-surfaces

port: SM_API curve utilities (primitive.api_curve_ops)
This commit is contained in:
bherbruck
2026-10-29 13:36:33 -04:00
committed by GitHub
7 changed files with 2679 additions and 0 deletions
@@ -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<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
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: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
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<P3> },
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<P3>,
/// `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<Crv>,
},
Order {
curves: Vec<Crv>,
},
}
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<Output, HarnessError> {
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<f64>, 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<f64>, 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<f64>, 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<Output>),
}
impl Made {
fn of<T>(r: solidcore::Result<T>, 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<Curve>) -> Made {
let made: solidcore::Result<Vec<Curve>> = match c {
Crv::Curve { points } => {
let pts: Vec<Vec3> = 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<BSplineCurve>) -> Made {
let made = match c {
Crv::Curve { points } => {
let pts: Vec<Vec3> = 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<f64>) -> 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<ParamMethod> {
Some(match code {
0 => ParamMethod::Uniform,
1 => ParamMethod::ChordLength,
2 => ParamMethod::Centripetal,
_ => return None,
})
}
fn rust(i: &In) -> Result<Output, HarnessError> {
let k = Kernel::default();
// the one curve of an offset or continuity case, else the case's record
let one = |c: &Crv| -> Result<Result<Curve, Box<Output>>, 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<Vec3> = 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<Crv> {
let n = corners.len();
let mut segs: Vec<Crv> = (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<P3> = (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<Value> {
serde_json::to_value(gen_case(rng)).ok()
}
fn cases(&self, ctx: &CaseContext) -> Result<Vec<Case>, HarnessError> {
let ctx = CaseContext {
seeds: ctx.seeds.max(MIN_GENERATED),
..ctx.clone()
};
default_cases(self, &ctx)
}
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
cpp(&case.parse()?)
}
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
rust(&case.parse()?)
}
}
@@ -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<InCurve>,
},
/// `SmApiOrderCurves(curves)`.
Order {
/// The curves (any kind; a rectangle adds its 4 segments).
curves: Vec<InCurve>,
},
}
fn put_curve(o: &mut Vec<f64>, 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<f64>, 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<Vec<f64>, 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<Vec<f64>, 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());
}
}
+1
View File
@@ -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;
@@ -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<BSplineCurve> {
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<Curve> {
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<ContinuityType> {
let domain = curve.natural_interval();
// (a NaN parameter is outside)
if !(domain.min()..=domain.max()).contains(&parameter) {
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<Vec<usize>> {
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<Point3> = (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<Curve> = 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);
}
}
File diff suppressed because it is too large Load Diff
@@ -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 <SmApiCurves.h>
#include <SmBSplineCurve.h>
#include <SmCircle.h>
#include <SmCurveTypes.h>
#include <SmExtent1d.h>
#include <SmLine.h>
#include <SmTArray.h>
#include <SmTypes.h>
#include <SmVector3d.h>
#include <nurbs.h>
#include <stdexcept>
#include <vector>
#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 <typename Tag, typename Tag::type M>
struct Rob
{
friend typename Tag::type get(Tag) { return M; }
};
struct BsForm
{
typedef SmBSplineCurveForm SmBSplineCurve::*type;
friend type get(BsForm);
};
template struct Rob<BsForm, &SmBSplineCurve::m_eBSplineCurveForm>;
struct BsKnotType
{
typedef SmKnotType SmBSplineCurve::*type;
friend type get(BsKnotType);
};
template struct Rob<BsKnotType, &SmBSplineCurve::m_eKnotType>;
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<SmBSplineCurve*> 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<double>& 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<SmPoint3d> 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<SmBSplineCurve*> 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<double>& 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<SmPoint3d> 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<SmBSplineCurve*> 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<double> 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
}
+12
View File
@@ -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."