mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
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:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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(¶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<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
|
||||
}
|
||||
@@ -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."
|
||||
Reference in New Issue
Block a user