Merge origin/main into unit/surface.surface_p1

Brings in surface.torus (PR #23). Conflict in surface/surface/p2.rs
imports: keep both StepSurface and Torus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BY8UpXKuMci15rAWGXsbsm
This commit is contained in:
bherbruck
2026-10-14 12:03:06 -04:00
11 changed files with 9481 additions and 18 deletions
+854
View File
@@ -0,0 +1,854 @@
//! Runner for unit `surface.torus`: `SmTorus` (construction through `CreateCanonical`,
//! `operator==`, `Copy`, `CreateOffsetSurface`, the STEP/NURB conversions, STEP
//! evaluation, iso curves, `RebuildSTEPFromNURBParameters`, `Transform`).
//!
//! C++: `solidcore_ffi::units::surface_torus` (upstream `source/SMLib/src/SmTorus.cpp`).
//! Rust: `solidcore::surface::torus`.
//!
//! A case is `torus` (placement, radii, flags, optional domain and generator circle)
//! plus `op` and its parameters. Both sides produce the same flat record (layout in
//! the FFI module docs), split into `build` (the construction status), `status` (the
//! op status) and `out` (the rest). Statuses are `0` (`SM_SUCCESS`) or the
//! `SmStatus`; `-1` on the Rust side for an error upstream cannot produce.
//!
//! Deferred cases: `Transform` other than the identity or a rejected scale
//! (`SmSurfOfRevolution::Transform`, `surface.surf_of_revolution_p3`).
use serde::{Deserialize, Serialize};
use serde_json::Value;
use solidcore::curve::curve::{Curve, CurveKind};
use solidcore::curve::ellipse::{Ellipse, EllipseOptions};
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::extent1d::Extent1d;
use solidcore::geom::extent2d::Extent2d;
use solidcore::geom::vector2d::Point2;
use solidcore::geom::vector3d::Vec3;
use solidcore::kernel::Kernel;
use solidcore::nurbs::b_spline_surface::{IsoCurveOptions, SurfParamType};
use solidcore::nurbs::crv_basic::Curve as NurbCurve;
use solidcore::surface::surface::p2::{
B_SPLINE_SURFACE_TYPE, SURF_OF_REVOLUTION_TYPE, TORUS_TYPE, TYPE,
};
use solidcore::surface::torus::{Torus, TorusOptions};
use solidcore::SolidError;
use solidcore_ffi::units::surface_torus as ffi;
use crate::case::CaseContext;
use crate::runner::default_cases;
use crate::{Case, HarnessError, Output, Rng, UnitDiff};
/// Generated cases per run, whatever `--seeds` says (it can only raise this).
pub const MIN_GENERATED: usize = 500;
/// `surface.torus` runner.
pub struct Runner;
fn harness(msg: impl Into<String>) -> HarnessError {
HarnessError::new(msg)
}
// ---- inputs -----------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CircleIn {
c: [f64; 3],
x: [f64; 3],
y: [f64; 3],
d: [f64; 2],
r: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TorusIn {
origin: [f64; 3],
x: [f64; 3],
y: [f64; 3],
major: f64,
minor: f64,
#[serde(default)]
swap: bool,
#[serde(default)]
inside_out: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
dom: Option<[f64; 4]>,
#[serde(default, skip_serializing_if = "Option::is_none")]
circle: Option<CircleIn>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
enum OpIn {
Build {},
EvalStep {
u: f64,
v: f64,
k: usize,
#[serde(default)]
point: bool,
},
ToStep {
u: f64,
v: f64,
},
ToNurbs {
u: f64,
v: f64,
},
Iso {
param: u8,
iso: f64,
#[serde(default)]
dom: Option<[f64; 4]>,
},
Offset {
dist: f64,
tol: f64,
#[serde(default)]
rebuild: bool,
},
Rebuild {},
Transform {
origin: [f64; 3],
x: [f64; 3],
y: [f64; 3],
#[serde(default)]
scale: Option<[f64; 3]>,
},
Eq {
other: TorusIn,
},
Copy {},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Inputs {
torus: TorusIn,
#[serde(flatten)]
op: OpIn,
}
// ---- C++ side -------------------------------------------------------------------------
fn ffi_torus(t: &TorusIn) -> ffi::TorusIn {
ffi::TorusIn {
origin: t.origin,
x: t.x,
y: t.y,
major: t.major,
minor: t.minor,
swap: t.swap,
inside_out: t.inside_out,
dom: t.dom,
circle: t.circle.as_ref().map(|c| ffi::CircleIn {
c: c.c,
x: c.x,
y: c.y,
d: c.d,
r: c.r,
}),
}
}
fn cpp(i: &Inputs) -> Result<Output, HarnessError> {
let op = match &i.op {
OpIn::Build {} => ffi::Op::Build,
OpIn::EvalStep { u, v, k, point } => ffi::Op::EvalStep {
u: *u,
v: *v,
k: *k,
point: *point,
},
OpIn::ToStep { u, v } => ffi::Op::ToStep { u: *u, v: *v },
OpIn::ToNurbs { u, v } => ffi::Op::ToNurbs { u: *u, v: *v },
OpIn::Iso { param, iso, dom } => ffi::Op::Iso {
param: *param,
iso: *iso,
dom: *dom,
},
OpIn::Offset { dist, tol, rebuild } => ffi::Op::Offset {
dist: *dist,
tol: *tol,
rebuild: *rebuild,
},
OpIn::Rebuild {} => ffi::Op::Rebuild,
OpIn::Transform {
origin,
x,
y,
scale,
} => ffi::Op::Transform {
origin: *origin,
x: *x,
y: *y,
scale: *scale,
},
OpIn::Eq { other } => ffi::Op::Eq {
other: Box::new(ffi_torus(other)),
},
OpIn::Copy {} => ffi::Op::Copy,
};
let flat = ffi::torus_op(&ffi_torus(&i.torus), &op).map_err(|e| harness(e.to_string()))?;
to_output(i, &flat)
}
/// Splits a record into `build`, `status` and `out`.
fn to_output(i: &Inputs, flat: &[f64]) -> Result<Output, HarnessError> {
let (b, rest) = flat
.split_first()
.ok_or_else(|| harness("empty record"))?;
let mut out = Output::new().values("build", vec![*b]);
if *b != 0.0 {
if !rest.is_empty() {
return Err(harness("record continues after a failed build"));
}
return Ok(out);
}
let mut rest = rest;
if !matches!(i.op, OpIn::Build {}) {
let (s, r) = rest
.split_first()
.ok_or_else(|| harness("record has no op status"))?;
out = out.values("status", vec![*s]);
rest = r;
}
Ok(out.values("out", rest.to_vec()))
}
// ---- Rust side ------------------------------------------------------------------------
fn flag(b: bool) -> f64 {
f64::from(u8::from(b))
}
fn v3(p: [f64; 3]) -> Vec3 {
let [x, y, z] = p;
Vec3::new(x, y, z)
}
fn push_v(o: &mut Vec<f64>, v: Vec3) {
o.extend([v.x, v.y, v.z]);
}
/// The `SmStatus` of a Rust error; `-1` for one upstream cannot return.
fn status(e: &SolidError) -> f64 {
match e {
SolidError::Upstream { code, .. } => f64::from(*code),
_ => -1.0,
}
}
/// `SC_ERR_ARG` (`harness/cpp/shim/sc_shim.h`): the shim rejected the arguments.
const SC_ERR_ARG: f64 = -1002.0;
/// Builds the torus; `Ok(Err(e))` when the construction fails.
fn rust_torus(k: &Kernel, t: &TorusIn) -> Result<solidcore::Result<Torus>, HarnessError> {
let circle = match &t.circle {
None => None,
Some(c) => {
let [d0, d1] = c.d;
let e = Ellipse::new_circle(
k,
v3(c.c),
v3(c.x),
v3(c.y),
Extent1d::from_min_max(d0, d1),
c.r,
&EllipseOptions::default(),
)
.map_err(|e| harness(format!("circle: {e}")))?;
Some(Curve::from(e))
}
};
let frame = Axis2Placement::new(v3(t.origin), v3(t.x), v3(t.y));
let dom = t.dom.map(|[a, b, c, d]| Extent2d::from_bounds(a, b, c, d));
Ok(Torus::create_canonical(
k,
&frame,
t.major,
t.minor,
TorusOptions {
swap_uv: t.swap,
inside_out: t.inside_out,
circle_nurb: circle,
},
dom.as_ref(),
))
}
fn nurb_of(c: &Curve) -> Option<&NurbCurve> {
match c.kind() {
CurveKind::BSpline(b) => b.gw_nurb(),
CurveKind::Ellipse(e) => e.base().gw_nurb(),
CurveKind::Line(l) => l.base().gw_nurb(),
CurveKind::Hyperbola(h) => h.base().gw_nurb(),
CurveKind::Parabola(p) => p.base().gw_nurb(),
_ => None,
}
}
fn step_interval(c: &Curve) -> Extent1d {
match c.kind() {
CurveKind::Ellipse(e) => e.step_interval(),
CurveKind::Line(l) => l.step_interval(),
CurveKind::Hyperbola(h) => h.step_interval(),
CurveKind::Parabola(p) => p.step_interval(),
_ => c.natural_interval(),
}
}
fn put_curve(o: &mut Vec<f64>, c: Option<&Curve>) {
o.push(flag(c.is_some()));
let Some(c) = c else { return };
o.push(c.type_code() as f64);
let ivl = step_interval(c);
o.extend([ivl.min(), ivl.max()]);
let cur = nurb_of(c);
o.push(flag(cur.is_some()));
if let Some(cur) = cur {
o.push(cur.degree() as f64);
o.push(cur.cpts().len() as f64);
for p in cur.cpts() {
o.extend([p.x, p.y, p.z, p.w]);
}
o.push(cur.knots().len() as f64);
o.extend_from_slice(cur.knots());
}
match c.kind() {
CurveKind::Ellipse(e) => {
o.push(1.0);
let p = e.position();
push_v(o, p.origin());
push_v(o, p.x_axis());
push_v(o, p.y_axis());
o.extend([e.x_radius(), e.y_radius(), flag(e.inside_out())]);
}
_ => o.push(0.0),
}
}
fn put_state(o: &mut Vec<f64>, t: &Torus) -> solidcore::Result<()> {
let s = t.base();
o.extend([
flag(t.is_kind_of(TORUS_TYPE)?),
flag(t.is_kind_of(SURF_OF_REVOLUTION_TYPE)?),
flag(t.is_kind_of(B_SPLINE_SURFACE_TYPE)?),
flag(t.is_kind_of(TYPE)?),
t.type_code() as f64,
flag(s.swap_uv()),
flag(s.planar_generator()),
flag(t.inside_out()),
t.major_radius(),
t.minor_radius(),
]);
let pos = s.position();
push_v(o, pos.origin());
push_v(o, pos.x_axis());
push_v(o, pos.y_axis());
let d = s.step_uv_domain();
o.extend([d.min().x, d.min().y, d.max().x, d.max().y]);
let mx = t.max_analytic_domain();
o.extend([mx.min().x, mx.min().y, mx.max().x, mx.max().y]);
let pc = s.polar_converter();
o.push(flag(s.have_polar_conversion()));
let angles = pc.angles();
o.push(angles.len() as f64);
if let (Some(a), Some(b)) = (angles.first(), angles.last()) {
o.extend([*a, *b]);
}
let polar_curve = s.polar_curve().map(|e| Curve::from(e.clone()));
put_curve(o, polar_curve.as_ref());
put_curve(o, s.gen_curve());
let sur = s.base().gw_nurb();
o.push(flag(sur.is_some()));
let Some(sur) = sur else { return Ok(()) };
let ix = sur.indices();
o.extend([ix.p, ix.q, ix.n + 1, ix.m + 1].map(|x| x as f64));
for row in sur.rows() {
for p in row {
o.extend([p.x, p.y, p.z, p.w]);
}
}
o.push(sur.u_knots().len() as f64);
o.extend_from_slice(sur.u_knots());
o.push(sur.v_knots().len() as f64);
o.extend_from_slice(sur.v_knots());
Ok(())
}
fn param_of(p: u8) -> SurfParamType {
if p == 0 {
SurfParamType::U
} else {
SurfParamType::V
}
}
/// What a Rust op run produced: the record continues (`Ok`), or the op failed with an
/// error whose status ends the record (`Err`). Ops that write the state after a
/// failure push it themselves.
type OpRun = solidcore::Result<()>;
/// Runs the op on the Rust torus, appending its status and outputs. `Err` from the
/// harness only; `Ok(Err(e))` when the op failed before writing its status.
fn rust_op(
k: &Kernel,
t: &mut Torus,
op: &OpIn,
o: &mut Vec<f64>,
) -> Result<OpRun, HarnessError> {
let r: OpRun = match op {
OpIn::Build {} => put_state(o, t),
OpIn::EvalStep { u, v, k: kk, point } => {
let uv = Point2::new(*u, *v);
if *point {
t.evaluate_step_point(uv).map(|p| {
o.push(0.0);
push_v(o, p);
})
} else {
t.evaluate_step(uv, *kk, *kk).map(|d| {
o.push(0.0);
for v in d {
push_v(o, v);
}
})
}
}
OpIn::ToStep { u, v } => t
.convert_uv_from_nurbs_to_step(k, Point2::new(*u, *v))
.map(|p| o.extend([0.0, p.x, p.y])),
OpIn::ToNurbs { u, v } => t
.convert_uv_from_step_to_nurbs(Point2::new(*u, *v))
.map(|p| o.extend([0.0, p.x, p.y])),
OpIn::Iso { param, iso, dom } => {
let dom = dom.map(|[a, b, c, d]| Extent2d::from_bounds(a, b, c, d));
// the shim's guard: the base extraction must succeed
let probe = t.base().base().create_iso_parametric_curve(
k,
param_of(*param),
*iso,
IsoCurveOptions {
domain: dom,
uv_iso_curve: false,
},
);
match probe {
Err(e) if e.is_unported() => Err(e),
Err(_) => {
o.push(SC_ERR_ARG);
Ok(())
}
Ok(_) => t
.create_iso_parametric_curve(k, param_of(*param), *iso, dom.as_ref())
.map(|c| {
o.extend([0.0, c.max_gap_3d]);
put_curve(o, Some(&c.curve));
}),
}
}
OpIn::Offset { dist, tol, rebuild } => {
t.create_offset_surface(k, *dist, *tol).and_then(|off| {
o.push(0.0);
o.push(flag(off.is_some()));
if let Some(mut off) = off {
if *rebuild {
match off.rebuild_step_from_nurb_parameters(k) {
Err(e) if e.is_unported() => return Err(e),
Err(e) => o.push(status(&e)),
Ok(()) => o.push(0.0),
}
}
put_state(o, &off)?;
}
Ok(())
})
}
OpIn::Rebuild {} => match t.rebuild_step_from_nurb_parameters(k) {
Err(e) if e.is_unported() => Err(e),
r => {
o.push(r.map_or_else(|e| status(&e), |()| 0.0));
put_state(o, t)
}
},
OpIn::Transform {
origin,
x,
y,
scale,
} => {
let a2p = Axis2Placement::new(v3(*origin), v3(*x), v3(*y));
match t.transform(k, &a2p, scale.map(v3)) {
Err(e) if e.is_unported() => Err(e),
r => {
o.push(r.map_or_else(|e| status(&e), |()| 0.0));
put_state(o, t)
}
}
}
OpIn::Eq { other } => match rust_torus(k, other)? {
Err(e) if e.is_unported() => Err(e),
Err(e) => {
o.push(status(&e));
Ok(())
}
Ok(other) => {
o.extend([0.0, flag(t.approx_eq(&other)), flag(other.approx_eq(t))]);
Ok(())
}
},
OpIn::Copy {} => {
let c = t.copy();
o.push(0.0);
put_state(o, &c).map(|()| o.push(flag(t.approx_eq(&c))))
}
};
Ok(r)
}
fn rust(i: &Inputs) -> Result<Output, HarnessError> {
let k = Kernel::default();
let mut o = Vec::new();
match rust_torus(&k, &i.torus)? {
Err(e) => {
if let Some(out) = Output::unported(&e) {
return Ok(out);
}
o.push(status(&e));
}
Ok(mut t) => {
o.push(0.0);
let at = o.len();
if let Err(e) = rust_op(&k, &mut t, &i.op, &mut o)? {
if let Some(out) = Output::unported(&e) {
return Ok(out);
}
o.truncate(at);
o.push(status(&e));
}
}
}
to_output(i, &o)
}
// ---- generation -----------------------------------------------------------------------
fn unit_vec(rng: &mut Rng) -> [f64; 3] {
loop {
let [x, y, z] = rng.point3(-1.0, 1.0);
let l = (x * x + y * y + z * z).sqrt();
if l > 0.2 && l <= 1.0 {
return [x / l, y / l, z / l];
}
}
}
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
let [ax, ay, az] = a;
let [bx, by, bz] = b;
[ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]
}
fn normalize(a: [f64; 3]) -> [f64; 3] {
let [x, y, z] = a;
let l = (x * x + y * y + z * z).sqrt();
[x / l, y / l, z / l]
}
fn add(a: [f64; 3], b: [f64; 3], s: f64) -> [f64; 3] {
let [ax, ay, az] = a;
let [bx, by, bz] = b;
[ax + s * bx, ay + s * by, az + s * bz]
}
/// A unit frame (X, Y); retried until both axes pass the FFI's unit check.
fn frame(rng: &mut Rng) -> ([f64; 3], [f64; 3]) {
match rng.below(3) {
0 => ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
1 => ([0.0, 1.0, 0.0], [0.0, 0.0, 1.0]),
_ => loop {
let z = unit_vec(rng);
let t = unit_vec(rng);
let y = normalize(cross3(z, t));
let x = normalize(cross3(y, z));
let unit = |v: [f64; 3]| {
let [a, b, c] = v;
((a * a + b * b + c * c) - 1.0).abs() <= 1e-12
};
if unit(x) && unit(y) {
return (x, y);
}
},
}
}
fn gen_torus(rng: &mut Rng) -> TorusIn {
let origin = if rng.chance(0.3) {
[0.0, 0.0, 0.0]
} else {
rng.point3(-5.0, 5.0)
};
let (x, y) = frame(rng);
let major = match rng.below(10) {
0 => *rng.choose(&[0.0, -1.0, 1e-13]).unwrap_or(&0.0),
_ => rng.uniform(0.5, 6.0),
};
let minor = match rng.below(12) {
0 => *rng.choose(&[0.0, -0.5]).unwrap_or(&0.0),
// spindle torus (minor >= major)
1 => major.abs() * rng.uniform(1.0, 1.5),
_ => major.abs() * rng.uniform(0.05, 0.9),
};
let angular = |rng: &mut Rng| -> [f64; 2] {
match rng.below(4) {
0 => [0.0, 360.0],
1 => {
let a = rng.uniform(-360.0, 0.0);
[a, a + rng.uniform(10.0, 360.0)]
}
_ => {
let a = rng.uniform(0.0, 200.0);
[a, (a + rng.uniform(10.0, 160.0)).min(360.0)]
}
}
};
let dom = rng.chance(0.5).then(|| {
let [u0, u1] = angular(rng);
let [v0, v1] = angular(rng);
[u0, v0, u1, v1]
});
// an explicit generator: the minor circle the constructor would make, over the V
// interval (or a little off it)
let circle = (rng.chance(0.15) && minor > 1e-6).then(|| {
let [_, v0, _, v1] = dom.unwrap_or([0.0, 0.0, 360.0, 360.0]);
let d = if rng.chance(0.7) {
[v0, v1]
} else {
[v0, (v1 - rng.uniform(1.0, 5.0)).max(v0 + 1.0)]
};
CircleIn {
c: add(origin, x, major),
x,
y: cross3(x, y),
d,
r: minor,
}
});
TorusIn {
origin,
x,
y,
major,
minor,
swap: rng.chance(0.4),
inside_out: rng.chance(0.4),
dom,
circle,
}
}
fn gen_case(rng: &mut Rng) -> Option<Inputs> {
let k = Kernel::default();
let torus = gen_torus(rng);
// the Rust torus sizes the parameters (both sides get the same numbers)
let built = rust_torus(&k, &torus).ok()?.ok();
let nd = built
.as_ref()
.map_or(Extent2d::from_bounds(0.0, 0.0, 1.0, 1.0), |b| {
b.base().base().natural_uv_domain()
});
let sd = built
.as_ref()
.map_or(Extent2d::from_bounds(0.0, 0.0, 360.0, 360.0), |b| {
b.base().step_uv_domain()
});
let pick = |rng: &mut Rng, lo: f64, hi: f64| -> f64 {
match rng.below(10) {
0 => lo,
1 => hi,
2 => lo - rng.uniform(0.0, 0.5) * (hi - lo),
_ => lo + rng.unit() * (hi - lo),
}
};
let op = match rng.below(13) {
0 => OpIn::Build {},
1..=3 => OpIn::EvalStep {
u: pick(rng, sd.min().x, sd.max().x),
v: pick(rng, sd.min().y, sd.max().y),
k: rng.below(4) as usize,
point: rng.chance(0.2),
},
4 => OpIn::ToStep {
u: pick(rng, nd.min().x, nd.max().x),
v: pick(rng, nd.min().y, nd.max().y),
},
5 => OpIn::ToNurbs {
u: pick(rng, sd.min().x, sd.max().x),
v: pick(rng, sd.min().y, sd.max().y),
},
6 | 7 => {
let param = rng.below(2) as u8;
let (lo, hi) = if param == 0 {
(nd.min().x, nd.max().x)
} else {
(nd.min().y, nd.max().y)
};
let dom = rng.chance(0.35).then(|| {
let a = nd.evaluate(rng.uniform(0.0, 0.4), rng.uniform(0.0, 0.4));
let b = nd.evaluate(rng.uniform(0.6, 1.0), rng.uniform(0.6, 1.0));
[a.x, a.y, b.x, b.y]
});
OpIn::Iso {
param,
iso: pick(rng, lo, hi),
dom,
}
}
8 => OpIn::Offset {
dist: rng.uniform(-1.5, 1.5) * torus.minor.abs(),
tol: *rng.choose(&[0.0, 1e-6, 1e-3, 0.1])?,
rebuild: rng.chance(0.4),
},
9 => OpIn::Rebuild {},
10 => {
let identity = rng.chance(0.4);
let (x, y) = if identity {
([1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
} else {
frame(rng)
};
let scale = match rng.below(4) {
0 => None,
1 => Some([1.0, 1.0, 1.0]),
2 => {
let s = rng.uniform(0.5, 2.0);
Some([s, s, s])
}
_ => Some([
rng.uniform(0.5, 2.0),
rng.uniform(0.5, 2.0),
*rng.choose(&[1.0, -1.0, 0.0])?,
]),
};
OpIn::Transform {
origin: if identity {
[0.0; 3]
} else {
rng.point3(-2.0, 2.0)
},
x,
y,
scale,
}
}
11 => {
let mut other = torus.clone();
match rng.below(5) {
0 => {}
1 => other.swap = !other.swap,
2 => other.minor += 1e-3,
3 => other.inside_out = !other.inside_out,
_ => other.origin = add(other.origin, [1e-3, 0.0, 0.0], 1.0),
}
OpIn::Eq { other }
}
_ => OpIn::Copy {},
};
Some(Inputs { torus, op })
}
impl UnitDiff for Runner {
fn id(&self) -> &'static str {
"surface.torus"
}
fn generate(&self, rng: &mut Rng) -> Option<Value> {
for _ in 0..8 {
if let Some(i) = gen_case(rng) {
return serde_json::to_value(i).ok();
}
}
None
}
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()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
fn op_name(op: &OpIn) -> &'static str {
match op {
OpIn::Build {} => "build",
OpIn::EvalStep { .. } => "eval_step",
OpIn::ToStep { .. } => "to_step",
OpIn::ToNurbs { .. } => "to_nurbs",
OpIn::Iso { .. } => "iso",
OpIn::Offset { .. } => "offset",
OpIn::Rebuild {} => "rebuild",
OpIn::Transform { .. } => "transform",
OpIn::Eq { .. } => "eq",
OpIn::Copy {} => "copy",
}
}
/// The generators reach a successful construction and a successful run of every op
/// on the Rust side.
#[test]
fn generated_cases_cover_every_op() {
let mut tally: BTreeMap<(String, String), usize> = BTreeMap::new();
for seed in 0..500u64 {
let mut rng = Rng::new(0x7075_0000 + seed);
let Some(i) = gen_case(&mut rng) else {
continue;
};
let out = rust(&i).expect("harness");
let kind = if let Some(d) = &out.deferred {
format!("deferred:{}", d.symbol)
} else {
let b = out.values.get("build").and_then(|v| v.first()).copied();
let st = out.values.get("status").and_then(|v| v.first()).copied();
format!("build:{b:?} status:{st:?}")
};
*tally.entry((op_name(&i.op).to_string(), kind)).or_default() += 1;
}
for (k, v) in &tally {
eprintln!("{k:?} {v}");
}
for op in [
"build",
"eval_step",
"to_step",
"to_nurbs",
"iso",
"offset",
"rebuild",
"transform",
"eq",
"copy",
] {
let ok = tally.iter().any(|((o, k), _)| {
o == op
&& (k == "build:Some(0.0) status:Some(0.0)"
|| k == "build:Some(0.0) status:None")
});
assert!(ok, "{op} never succeeds");
}
}
}
@@ -0,0 +1,354 @@
//! FFI wrappers for unit `surface.torus`.
//!
//! Shim: `harness/cpp/shim/surface_torus.cpp` (`sc_surface_torus_eval`).
//! Upstream: `source/SMLib/src/SmTorus.cpp`.
//!
//! [`torus_op`] builds a torus with `SmTorus::CreateCanonical` from a [`TorusIn`] and
//! runs one [`Op`] on it. The shim's flat record is the construction status (`0` =
//! `SM_SUCCESS`, else the `SmStatus`), then on success the op's record:
//!
//! * `Build`: the state (no status). A state is `is_torus is_rev is_bspline is_surface
//! type swap planar inside_out major minor O[3] X[3] Y[3] D[4] MAXD[4] have_polar
//! angle_count [first last] polar_curve gen_curve has_nurb [p q nu nv cpts(4 nu nv,
//! v fastest) nku KU nkv KV]`; a curve is `has [type step0 step1 has_nurb [p n
//! cpts(4 n) nk knots] is_ellipse [O[3] X[3] Y[3] rx ry inside_out]]`.
//! * the other ops: their status, then on success `EvalStep`: `(k+1)^2` vectors (or the
//! point); `ToStep`, `ToNurbs`: `u v`; `Iso`: `gap curve` (the shim answers
//! `SC_ERR_ARG` without status when `SmBSplineSurface::CreateIsoParametricCurve`
//! fails, since the torus override then reads an uninitialised curve); `Offset`:
//! `has [rebuild_status] state`; `Rebuild`: the state (also after a failure);
//! `Transform`: the state (also after a failure); `Eq`: the other torus's
//! construction status, then on success `this == other`, `other == this`; `Copy`:
//! `state copy == this`.
//!
//! The wrappers accept only what upstream handles without undefined behaviour: finite
//! values, unit and independent placement axes (the base constructor passes the Y axis
//! to `IsOnPlane`, whose output a longer one leaves uninitialised), angular domains
//! inside `[-360, 360]` at most 360 long, generator circles with independent axes and a
//! positive radius, derivative orders up to 3.
use crate::{call_with_buffer, FfiError};
mod sys {
extern "C" {
pub fn sc_surface_torus_eval(
op: i32,
input: *const f64,
n_in: i64,
out: *mut f64,
cap: i64,
out_len: *mut i64,
) -> i32;
}
}
/// `SmCircle(c, x, y, [d0, d1], r, 3)`, the optional generator (`pOptCircleNurb`).
#[derive(Debug, Clone, PartialEq)]
pub struct CircleIn {
/// Centre.
pub c: [f64; 3],
/// X axis.
pub x: [f64; 3],
/// Y axis.
pub y: [f64; 3],
/// Angular domain in degrees.
pub d: [f64; 2],
/// Radius.
pub r: f64,
}
/// `SmTorus::CreateCanonical(SmAxis2Placement(origin, x, y), major, minor, .., swap,
/// inside_out, circle, dom)`.
#[derive(Debug, Clone, PartialEq)]
pub struct TorusIn {
/// Torus centre.
pub origin: [f64; 3],
/// Major circle start direction.
pub x: [f64; 3],
/// Major circle 90 degree direction.
pub y: [f64; 3],
/// Major radius.
pub major: f64,
/// Minor radius.
pub minor: f64,
/// `bSwapUV`.
pub swap: bool,
/// `bInsideOut`.
pub inside_out: bool,
/// Optional STEP domain `umin vmin umax vmax` in degrees.
pub dom: Option<[f64; 4]>,
/// Optional generator.
pub circle: Option<CircleIn>,
}
/// The upstream member run on the torus.
#[derive(Debug, Clone, PartialEq)]
pub enum Op {
/// The construction state only.
Build,
/// `EvaluateSTEP(uv, k, k, ..)`, or `EvaluateSTEPPoint` when `point`.
EvalStep {
/// Major angle in degrees.
u: f64,
/// Minor angle in degrees.
v: f64,
/// Derivative order (both directions), at most 3.
k: usize,
/// Use `EvaluateSTEPPoint`.
point: bool,
},
/// `ConvertUVFromNURBSToSTEP`.
ToStep {
/// NURB u.
u: f64,
/// NURB v.
v: f64,
},
/// `ConvertUVFromSTEPToNURBS`.
ToNurbs {
/// STEP u.
u: f64,
/// STEP v.
v: f64,
},
/// `CreateIsoParametricCurve`.
Iso {
/// `0` = `SM_SP_U`, `1` = `SM_SP_V`.
param: u8,
/// Iso parameter.
iso: f64,
/// Optional trim domain.
dom: Option<[f64; 4]>,
},
/// `CreateOffsetSurface`, then optionally `RebuildSTEPFromNURBParameters` on the
/// offset torus.
Offset {
/// Signed offset distance.
dist: f64,
/// Approximation tolerance.
tol: f64,
/// Rebuild the offset torus.
rebuild: bool,
},
/// `RebuildSTEPFromNURBParameters`.
Rebuild,
/// `Transform(SmAxis2Placement(origin, x, y), scale)`.
Transform {
/// Placement origin.
origin: [f64; 3],
/// Placement X axis.
x: [f64; 3],
/// Placement Y axis.
y: [f64; 3],
/// Optional scale.
scale: Option<[f64; 3]>,
},
/// `operator==` with another torus.
Eq {
/// The other torus.
other: Box<TorusIn>,
},
/// `Copy`.
Copy,
}
impl Op {
fn code(&self) -> i32 {
match self {
Op::Build => 0,
Op::EvalStep { .. } => 1,
Op::ToStep { .. } => 2,
Op::ToNurbs { .. } => 3,
Op::Iso { .. } => 4,
Op::Offset { .. } => 5,
Op::Rebuild => 6,
Op::Transform { .. } => 7,
Op::Eq { .. } => 8,
Op::Copy => 9,
}
}
}
fn bad(msg: impl Into<String>) -> FfiError {
FfiError::InvalidArgument(msg.into())
}
fn finite(v: &[f64], what: &str) -> Result<(), FfiError> {
if v.iter().all(|x| x.is_finite()) {
Ok(())
} else {
Err(bad(format!("{what}: non-finite value")))
}
}
fn len(v: [f64; 3]) -> f64 {
let [x, y, z] = v;
(x * x + y * y + z * z).sqrt()
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
let [ax, ay, az] = a;
let [bx, by, bz] = b;
[ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]
}
/// Checks two axes are long enough and far from parallel.
fn axes(x: [f64; 3], y: [f64; 3], what: &str) -> Result<(), FfiError> {
finite(&x, what)?;
finite(&y, what)?;
let (lx, ly) = (len(x), len(y));
if lx < 1e-6 || ly < 1e-6 || len(cross(x, y)) < 1e-3 * lx * ly {
return Err(bad(format!("{what}: degenerate or parallel axes")));
}
Ok(())
}
/// `SM_EFF_ZERO`.
const EFF_ZERO: f64 = 1e-12;
/// Checks a vector is a unit vector within `SM_EFF_ZERO` on its squared length.
fn unit(v: [f64; 3], what: &str) -> Result<(), FfiError> {
let [x, y, z] = v;
if ((x * x + y * y + z * z) - 1.0).abs() > EFF_ZERO {
return Err(bad(format!("{what}: not a unit vector")));
}
Ok(())
}
fn domain(d: [f64; 4], what: &str) -> Result<(), FfiError> {
finite(&d, what)?;
let [a, b, c, e] = d;
if !(a <= c && b <= e) {
return Err(bad(format!("{what}: domain not ordered")));
}
Ok(())
}
fn angular(d0: f64, d1: f64, what: &str) -> Result<(), FfiError> {
finite(&[d0, d1], what)?;
if !(d0 < d1 && d0 >= -360.0 && d1 <= 360.0 && d1 - d0 <= 360.0) {
return Err(bad(format!("{what}: bad angular domain")));
}
Ok(())
}
fn flag(b: bool) -> f64 {
f64::from(u8::from(b))
}
fn push_torus(o: &mut Vec<f64>, t: &TorusIn) -> Result<(), FfiError> {
finite(&t.origin, "origin")?;
axes(t.x, t.y, "placement")?;
unit(t.x, "placement X")?;
unit(t.y, "placement Y")?;
finite(&[t.major, t.minor], "radii")?;
o.extend_from_slice(&t.origin);
o.extend_from_slice(&t.x);
o.extend_from_slice(&t.y);
o.extend([t.major, t.minor, flag(t.swap), flag(t.inside_out)]);
o.push(flag(t.dom.is_some()));
let d = t.dom.unwrap_or([0.0, 0.0, 360.0, 360.0]);
domain(d, "torus domain")?;
let [u0, v0, u1, v1] = d;
angular(u0, u1, "torus domain u")?;
angular(v0, v1, "torus domain v")?;
o.extend_from_slice(&d);
o.push(flag(t.circle.is_some()));
match &t.circle {
Some(CircleIn { c, x, y, d, r }) => {
finite(c, "circle")?;
axes(*x, *y, "circle")?;
let [d0, d1] = *d;
angular(d0, d1, "circle")?;
if !(r.is_finite() && *r > 1e-6) {
return Err(bad("circle: radius"));
}
o.extend_from_slice(c);
o.extend_from_slice(x);
o.extend_from_slice(y);
o.extend([d0, d1, *r]);
}
None => o.extend([0.0; 12]),
}
Ok(())
}
fn push_op(o: &mut Vec<f64>, op: &Op) -> Result<(), FfiError> {
match op {
Op::Build | Op::Rebuild | Op::Copy => {}
Op::EvalStep { u, v, k, point } => {
finite(&[*u, *v], "eval")?;
if *k > 3 {
return Err(bad("eval: order above 3"));
}
o.extend([*u, *v, *k as f64, flag(*point)]);
}
Op::ToStep { u, v } | Op::ToNurbs { u, v } => {
finite(&[*u, *v], "convert")?;
o.extend([*u, *v]);
}
Op::Iso { param, iso, dom } => {
if *param > 1 {
return Err(bad("iso: param"));
}
finite(&[*iso], "iso")?;
o.extend([f64::from(*param), *iso, flag(dom.is_some())]);
let d = dom.unwrap_or([0.0, 0.0, 0.0, 0.0]);
domain(d, "iso domain")?;
o.extend_from_slice(&d);
}
Op::Offset { dist, tol, rebuild } => {
finite(&[*dist, *tol], "offset")?;
o.extend([*dist, *tol, flag(*rebuild)]);
}
Op::Transform {
origin,
x,
y,
scale,
} => {
finite(origin, "transform")?;
axes(*x, *y, "transform")?;
let s = scale.unwrap_or([1.0, 1.0, 1.0]);
finite(&s, "transform scale")?;
o.extend_from_slice(origin);
o.extend_from_slice(x);
o.extend_from_slice(y);
o.push(flag(scale.is_some()));
o.extend_from_slice(&s);
}
Op::Eq { other } => push_torus(o, other)?,
}
Ok(())
}
/// Builds the torus `t` and runs `op` on it; returns the shim's record (layout in the
/// module docs).
///
/// # Errors
///
/// [`FfiError::InvalidArgument`] for input the shim must not see; [`FfiError::Status`]
/// for a shim failure.
pub fn torus_op(t: &TorusIn, op: &Op) -> Result<Vec<f64>, FfiError> {
let mut input = Vec::new();
push_torus(&mut input, t)?;
push_op(&mut input, op)?;
let code = op.code();
call_with_buffer(4096, |buf, len| {
// SAFETY: input is valid for input.len() reads and holds exactly what the shim
// reads for `code` (built and validated above: finite values, unit independent
// placement axes, non-degenerate circles, ordered angular domains, orders up to
// 3); buf is valid for buf.len() writes and the shim honours the capacity; len
// is valid.
unsafe {
sys::sc_surface_torus_eval(
code,
input.as_ptr(),
input.len() as i64,
buf.as_mut_ptr(),
buf.len() as i64,
&raw mut *len,
)
}
})
}
+1
View File
@@ -16,6 +16,7 @@ pub mod surf_of_revolution;
#[allow(clippy::module_inception)]
pub mod surface;
pub mod surface_cache;
pub mod torus;
pub mod trim_srf_cache;
pub mod twist_volume;
pub mod unbend_volume;
@@ -38,7 +38,10 @@
//! - **Derived class.** `SmSurfOfRevolution` derives from `SmBSplineSurface`; the base
//! object is the [`SurfOfRevolution::base`] field. The surface joins
//! [`crate::surface::surface::SurfaceKind`] with the units porting its remaining
//! virtual overrides (`_p2`, `_p3`).
//! virtual overrides (`_p2`, `_p3`). The NURB and polar-converter rebuilds reach
//! `CreateIsoParametricCurve` through a `SweepIsoFn` so that a derived class
//! (`SmTorus`, [`crate::surface::torus`]) supplies its override, as upstream's
//! virtual call does.
//! - **Generator.** `m_pGenCurve` is a [`Curve`] of a kind derived from
//! `SmBSplineCurve` (B-spline, ellipse, line, hyperbola, parabola); the other kinds
//! are rejected (`InvalidInput`) where upstream casts. It is only `None` for the
@@ -243,6 +246,12 @@ fn gen_with_out_of_bounds(c: &Curve, enabled: bool) -> Option<Curve> {
}
}
/// The sweep iso-curve maker of the NURB and polar-converter rebuilds: upstream's
/// virtual `CreateIsoParametricCurve(.., eSurfParam, dIsoParameter, ..)` without trim
/// domain, of [`SurfOfRevolution`] or of a derived class (`SmTorus`).
pub(crate) type SweepIsoFn<'a> =
&'a dyn Fn(&SurfOfRevolution, &Kernel, SurfParamType, f64) -> Result<Curve>;
// ---- results ---------------------------------------------------------------------------
/// The result of [`SurfOfRevolution::create_iso_parametric_curve`].
@@ -507,6 +516,18 @@ impl SurfOfRevolution {
&mut self,
k: &Kernel,
opt_sweep_curve: Option<&Curve>,
) -> Result<()> {
self.make_nurb_with_sweep_params_using(k, opt_sweep_curve, &Self::own_sweep_iso)
}
/// [`Self::make_nurb_with_sweep_params`] with the polar converter rebuilt from the
/// sweep iso curves of `iso` (upstream's virtual `CreateIsoParametricCurve`: a
/// derived class such as `SmTorus` passes its override).
pub(crate) fn make_nurb_with_sweep_params_using(
&mut self,
k: &Kernel,
opt_sweep_curve: Option<&Curve>,
iso: SweepIsoFn<'_>,
) -> Result<()> {
// locals
let surf_sweep_step_ivl = self.anal_uv_domain.u_interval();
@@ -552,20 +573,38 @@ impl SurfOfRevolution {
self.base.nurb = tmp.nurb;
// Set up the Polar conversion - create a sweep isoParametric curve
ignore_status(self.rebuild_polar_converter(k))?;
ignore_status(self.rebuild_polar_converter(k, iso))?;
Ok(())
}
/// This class's `CreateIsoParametricCurve` without trim domain, as a [`SweepIsoFn`].
fn own_sweep_iso(
&self,
k: &Kernel,
surf_param: SurfParamType,
iso_parameter: f64,
) -> Result<Curve> {
Ok(self
.create_iso_parametric_curve(k, surf_param, iso_parameter, None)?
.curve)
}
/// Rebuilds the polar converter from the sweep iso curve at the NURB domain's
/// generator-direction start (or, when that circle is degenerate, at 45% of the
/// domain) (`RebuildPolarConverter`).
/// domain) (`RebuildPolarConverter`), the iso curves made by `iso` (upstream's
/// virtual `CreateIsoParametricCurve`: this class's
/// [`Self::create_iso_parametric_curve`] or a derived class's override).
///
/// # Errors
///
/// `SM_ERR` when both iso curves are degenerate; those of the iso curves (upstream
/// then dereferences a null curve) and of the table set-up; `InvalidInput` when the
/// iso curve is not a circle (upstream casts it).
pub(crate) fn rebuild_polar_converter(&mut self, k: &Kernel) -> Result<()> {
pub(crate) fn rebuild_polar_converter(
&mut self,
k: &Kernel,
iso: SweepIsoFn<'_>,
) -> Result<()> {
// Try the beginning of the curve if possible
let nurb_domain = self.base.natural_uv_domain();
let nurb_min_uv = nurb_domain.min();
@@ -576,9 +615,7 @@ impl SurfOfRevolution {
} else {
(SurfParamType::V, nurb_min_uv.y)
};
let mut sweep_iso = self
.create_iso_parametric_curve(k, param, sweep_param, None)?
.curve;
let mut sweep_iso = iso(self, k, param, sweep_param)?;
let mut iso_degenerate = sweep_iso.is_degenerate(EFF_ZERO, None)?;
if iso_degenerate {
@@ -591,9 +628,7 @@ impl SurfOfRevolution {
} else {
nurb_mid_uv.y
};
sweep_iso = self
.create_iso_parametric_curve(k, param, sweep_param, None)?
.curve;
sweep_iso = iso(self, k, param, sweep_param)?;
iso_degenerate = sweep_iso.is_degenerate(EFF_ZERO, None)?;
}
+9 -2
View File
@@ -171,6 +171,7 @@ use crate::nurbs::knots::Side;
use crate::surface::curve_bounded_surface::CurveBoundedSurface;
use crate::surface::plane::Plane;
use crate::surface::surface::p1::StepSurface;
use crate::surface::torus::Torus;
use crate::topology::database_io::DatabaseReader;
// ---- type codes ---------------------------------------------------------------------
@@ -939,8 +940,14 @@ impl Surface {
"SmSphere::ReadFromDB",
"surface.sphere",
)),
// UNPORTED(surface.torus): SmTorus::ReadFromDB
TORUS_TYPE => Err(SolidError::unported("SmTorus::ReadFromDB", "surface.torus")),
// (SmTorus::ReadFromDB reads the torus record, then needs the unported
// SmSurfOfRevolution record; the torus is not a SurfaceKind yet)
TORUS_TYPE => Err(Torus::read_from_db(db, version).err().unwrap_or_else(|| {
SolidError::unported(
"SmSurfOfRevolution::ReadFromDB",
"surface.surf_of_revolution_p3",
)
})),
CURVE_BOUNDED_SURFACE_TYPE => Ok(Self::from(CurveBoundedSurface::read_from_db(
k, db, version,
)?)),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+417
View File
@@ -0,0 +1,417 @@
// SPDX-License-Identifier: Apache-2.0
// difftest shim for unit `surface.torus`.
// Wraps the SmTorus members owned by the unit (source/SMLib/src/SmTorus.cpp): CreateCanonical
// (the constructor and MakeNurb), operator==, Copy, CreateOffsetSurface,
// ConvertUVFromSTEPToNURBS, ConvertUVFromNURBSToSTEP, EvaluateSTEP, EvaluateSTEPPoint,
// CreateIsoParametricCurve, RebuildSTEPFromNURBParameters, Transform, GetMaxAnalyticDomain,
// GetInsideOut and IsKindOf.
//
// One entry point, sc_surface_torus_eval(op, in, n_in, out, cap, out_len). Layouts are
// documented in crates/solidcore-ffi/src/units/surface_torus.rs, which validates every input
// so upstream only touches valid memory. `out` starts with the status of CreateCanonical.
#include "sc_shim.h"
#include "StdAfx.h"
#include <SmAxis2Placement.h>
#include <SmBSplineCurve.h>
#include <SmBSplineSurface.h>
#include <SmCircle.h>
#include <SmContext.h>
#include <SmCurve.h>
#include <SmEllipse.h>
#include <SmExtent1d.h>
#include <SmExtent2d.h>
#include <SmNurbsCrv.h>
#include <SmNurbsSrf.h>
#include <SmSurfOfRevolution.h>
#include <SmSurface.h>
#include <SmTorus.h>
#include <SmVector2d.h>
#include <SmVector3d.h>
#include <nurbs.h>
#include <vector>
namespace
{
// Ops (keep in sync with crates/solidcore-ffi/src/units/surface_torus.rs).
enum : int32_t
{
OP_BUILD = 0,
OP_EVAL_STEP = 1,
OP_TO_STEP = 2,
OP_TO_NURBS = 3,
OP_ISO = 4,
OP_OFFSET = 5,
OP_REBUILD = 6,
OP_TRANSFORM = 7,
OP_EQ = 8,
OP_COPY = 9,
};
// Reads the protected members through pointers to members named in a derived class.
struct Peek : SmTorus
{
static SmBoolean SmSurfOfRevolution::*planar() { return &Peek::m_bPlanarGenerator; }
static SmPolarConversion SmSurfOfRevolution::*polar() { return &Peek::m_vPolarConverter; }
};
struct Rd
{
const double* in;
int64_t at;
double num() { return in[at++]; }
int64_t idx() { return (int64_t)in[at++]; }
bool flag() { return in[at++] != 0.0; }
SmVector3d vec()
{
double x = num();
double y = num();
double z = num();
return SmVector3d(x, y, z);
}
SmExtent2d domain()
{
double a = num();
double b = num();
double c = num();
double d = num();
return SmExtent2d(a, b, c, d);
}
};
double st(SmStatus s)
{
return s == SM_SUCCESS ? 0.0 : (double)s;
}
double flg(bool b)
{
return b ? 1.0 : 0.0;
}
void put_vec(std::vector<double>& o, const SmVector3d& v)
{
o.push_back(v.x);
o.push_back(v.y);
o.push_back(v.z);
}
// A torus block `O[3] X[3] Y[3] major minor swap inside_out has_dom D[4] has_circle
// [c[3] x[3] y[3] d0 d1 r]`: SmTorus::CreateCanonical(SmAxis2Placement(O, X, Y), ..).
// Writes the status; NULL on failure.
SmTorus* read_torus(Rd& rd, const SmContext& ctx, std::vector<double>& o)
{
SmPoint3d org = rd.vec();
SmVector3d x = rd.vec();
SmVector3d y = rd.vec();
double major = rd.num();
double minor = rd.num();
bool swap = rd.flag();
bool inside = rd.flag();
bool has_dom = rd.flag();
SmExtent2d dom = rd.domain();
bool has_circle = rd.flag();
SmCircle* circle = NULL;
if (has_circle)
{
SmPoint3d c = rd.vec();
SmVector3d cx = rd.vec();
SmVector3d cy = rd.vec();
double d0 = rd.num();
double d1 = rd.num();
double r = rd.num();
circle = new (ctx) SmCircle(c, cx, cy, SmExtent1d(d0, d1), r, 3, &ctx);
}
else
{
for (int i = 0; i < 12; i++)
rd.num();
}
SmTorus* t = NULL;
SmStatus stt = SmTorus::CreateCanonical(ctx, SmAxis2Placement(org, x, y), major, minor, t,
swap ? TRUE : FALSE, inside ? TRUE : FALSE, circle,
has_dom ? &dom : NULL);
o.push_back(st(stt));
if (stt != SM_SUCCESS || t == NULL)
{
// the torus owns the circle once constructed (and deletes it on failure); before
// that (a degenerate radius) nobody does
if (t == NULL && circle != NULL && !(minor > SM_EFF_ZERO && major > SM_EFF_ZERO))
delete circle;
delete t;
return NULL;
}
return t;
}
// `has [type step0 step1 has_nurb [p n cpts(4 n) nk knots]] [ellipse O X Y rx ry inside]`.
void put_curve(std::vector<double>& o, const SmCurve* c)
{
o.push_back(flg(c != NULL));
if (!c)
return;
o.push_back((double)c->GetType());
SmExtent1d ivl = c->GetSTEPInterval();
o.push_back(ivl.GetMin());
o.push_back(ivl.GetMax());
const SmBSplineCurve* b =
c->IsKindOf(SmBSplineCurve_TYPE) ? (const SmBSplineCurve*)c : NULL;
const gw_CURVE* cur = b ? b->GetGwNurbPointer() : NULL;
o.push_back(flg(cur != NULL));
if (cur)
{
o.push_back((double)cur->p);
o.push_back((double)(cur->pol->n + 1));
for (gw_INDEX i = 0; i <= cur->pol->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);
}
o.push_back((double)(cur->knt->m + 1));
for (gw_INDEX i = 0; i <= cur->knt->m; i++)
o.push_back(cur->knt->U[i]);
}
const SmEllipse* e = c->IsKindOf(SmEllipse_TYPE) ? (const SmEllipse*)c : NULL;
o.push_back(flg(e != NULL));
if (e)
{
const SmAxis2Placement& p = e->GetPosition();
put_vec(o, p.GetOriginRef());
put_vec(o, p.GetXAxisRef());
put_vec(o, p.GetYAxisRef());
o.push_back(e->GetXRadius());
o.push_back(e->GetYRadius());
o.push_back(flg(e->GetInsideOut()));
}
}
// The torus state.
void put_state(std::vector<double>& o, const SmTorus* s)
{
o.push_back(flg(s->IsKindOf(SmTorus_TYPE)));
o.push_back(flg(s->IsKindOf(SmSurfOfRevolution_TYPE)));
o.push_back(flg(s->IsKindOf(SmBSplineSurface_TYPE)));
o.push_back(flg(s->IsKindOf(SmSurface_TYPE)));
o.push_back((double)s->GetType());
o.push_back(flg(s->GetSwapUV()));
o.push_back(flg(s->*Peek::planar()));
o.push_back(flg(s->GetInsideOut()));
o.push_back(s->GetMajorRadius());
o.push_back(s->GetMinorRadius());
const SmAxis2Placement& pos = s->GetPosition();
put_vec(o, pos.GetOriginRef());
put_vec(o, pos.GetXAxisRef());
put_vec(o, pos.GetYAxisRef());
SmExtent2d d = s->GetSTEPUVDomain();
o.push_back(d.GetMin().x);
o.push_back(d.GetMin().y);
o.push_back(d.GetMax().x);
o.push_back(d.GetMax().y);
SmExtent2d mx = s->GetMaxAnalyticDomain();
o.push_back(mx.GetMin().x);
o.push_back(mx.GetMin().y);
o.push_back(mx.GetMax().x);
o.push_back(mx.GetMax().y);
const SmPolarConversion& pc = s->*Peek::polar();
o.push_back(flg(s->HavePolarConversion()));
ULONG cnt = pc.GetAngleCount();
o.push_back((double)cnt);
if (cnt > 0)
{
o.push_back(pc.GetFirstAngle());
o.push_back(pc.GetLastAngle());
}
put_curve(o, pc.GetCurve());
put_curve(o, s->GetGenCurve());
const gw_SURFACE* sur = s->GetGwNurbPointer();
o.push_back(flg(sur != NULL));
if (!sur)
return;
o.push_back((double)sur->p);
o.push_back((double)sur->q);
o.push_back((double)(sur->net->n + 1));
o.push_back((double)(sur->net->m + 1));
for (gw_INDEX i = 0; i <= sur->net->n; i++)
for (gw_INDEX j = 0; j <= sur->net->m; j++)
{
o.push_back(sur->net->Pw[i][j].x);
o.push_back(sur->net->Pw[i][j].y);
o.push_back(sur->net->Pw[i][j].z);
o.push_back(sur->net->Pw[i][j].w);
}
o.push_back((double)(sur->knu->m + 1));
for (gw_INDEX i = 0; i <= sur->knu->m; i++)
o.push_back(sur->knu->U[i]);
o.push_back((double)(sur->knv->m + 1));
for (gw_INDEX i = 0; i <= sur->knv->m; i++)
o.push_back(sur->knv->U[i]);
}
void torus_op(int32_t op, Rd& rd, const SmContext& ctx, SmTorus* s, std::vector<double>& o)
{
switch (op)
{
case OP_BUILD: put_state(o, s); break;
case OP_EVAL_STEP:
{
double u = rd.num();
double v = rd.num();
ULONG k = (ULONG)rd.idx();
bool point = rd.flag();
if (point)
{
SmPoint3d p(0.0, 0.0, 0.0);
SmStatus stt = s->EvaluateSTEPPoint(SmPoint2d(u, v), p);
o.push_back(st(stt));
if (stt == SM_SUCCESS)
put_vec(o, p);
break;
}
std::vector<SmVector3d> d((k + 1) * (k + 1), SmVector3d(0.0, 0.0, 0.0));
SmStatus stt = s->EvaluateSTEP(SmPoint2d(u, v), k, k, TRUE, TRUE, TRUE, d.data());
o.push_back(st(stt));
if (stt == SM_SUCCESS)
for (size_t i = 0; i < d.size(); i++)
put_vec(o, d[i]);
break;
}
case OP_TO_STEP:
case OP_TO_NURBS:
{
double u = rd.num();
double v = rd.num();
SmPoint2d r(0.0, 0.0);
SmStatus stt = op == OP_TO_STEP ? s->ConvertUVFromNURBSToSTEP(SmPoint2d(u, v), r)
: s->ConvertUVFromSTEPToNURBS(SmPoint2d(u, v), r);
o.push_back(st(stt));
if (stt == SM_SUCCESS)
{
o.push_back(r.x);
o.push_back(r.y);
}
break;
}
case OP_ISO:
{
SmSurfParamType param = rd.idx() == 0 ? SM_SP_U : SM_SP_V;
double iso = rd.num();
bool has_dom = rd.flag();
SmExtent2d dom = rd.domain();
// the override ignores the status of the base extraction and then reads its
// curve: reject the arguments it fails on
SmBSplineCurve* probe = NULL;
SmStatus pst = s->SmBSplineSurface::CreateIsoParametricCurve(
ctx, param, iso, 0.0, probe, has_dom ? &dom : NULL, NULL, NULL);
bool ok = pst == SM_SUCCESS && probe != NULL;
delete probe;
if (!ok)
{
o.push_back((double)SC_ERR_ARG);
break;
}
SmBSplineCurve* c = NULL;
double gap = -1.0;
SmStatus stt = s->CreateIsoParametricCurve(ctx, param, iso, 0.0, c,
has_dom ? &dom : NULL, &gap, NULL);
o.push_back(st(stt));
if (stt == SM_SUCCESS)
{
o.push_back(gap);
put_curve(o, c);
}
delete c;
break;
}
case OP_OFFSET:
{
double dist = rd.num();
double tol = rd.num();
bool rebuild = rd.flag();
SmSurface* off = NULL;
SmStatus stt = s->CreateOffsetSurface(ctx, dist, tol, off);
o.push_back(st(stt));
if (stt == SM_SUCCESS)
{
o.push_back(flg(off != NULL));
if (off)
{
SmTorus* t = (SmTorus*)off;
if (rebuild)
o.push_back(st(t->RebuildSTEPFromNURBParameters()));
put_state(o, t);
}
}
delete off;
break;
}
case OP_REBUILD:
{
SmStatus stt = s->RebuildSTEPFromNURBParameters();
o.push_back(st(stt));
put_state(o, s);
break;
}
case OP_TRANSFORM:
{
SmPoint3d org = rd.vec();
SmVector3d x = rd.vec();
SmVector3d y = rd.vec();
bool has_scale = rd.flag();
SmVector3d sc = rd.vec();
SmAxis2Placement a2p(org, x, y);
SmStatus stt = s->Transform(a2p, has_scale ? &sc : NULL);
o.push_back(st(stt));
put_state(o, s);
break;
}
case OP_EQ:
{
std::vector<double> o2;
SmTorus* other = read_torus(rd, ctx, o2);
o.push_back(o2.empty() ? -1.0 : o2[0]);
if (other)
{
o.push_back(flg(*s == *other));
o.push_back(flg(*other == *s));
delete other;
}
break;
}
case OP_COPY:
{
SmSurface* c = NULL;
SmStatus stt = s->Copy(ctx, c);
o.push_back(st(stt));
if (stt == SM_SUCCESS && c)
{
put_state(o, (SmTorus*)c);
o.push_back(flg(*s == *c));
}
delete c;
break;
}
default: o.push_back((double)SC_ERR_ARG); break;
}
}
} // namespace
SC_EXPORT int32_t sc_surface_torus_eval(int32_t op, const double* in, int64_t n_in, double* out,
int64_t cap, int64_t* out_len)
{
(void)n_in;
SC_TRY
SmContext ctx;
std::vector<double> o;
Rd rd{in, 0};
SmTorus* s = read_torus(rd, ctx, o);
if (s)
{
torus_op(op, rd, ctx, s, o);
delete s;
}
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
SC_CATCH
}
+3 -4
View File
@@ -2,7 +2,7 @@
Generated from `plan/modules.toml` by `harness/plan/progress.py`; do not edit by hand.
**126 / 261 units ported** (83 green, 43 partial), **161,740 / 387,903 upstream lines (41.7%)**. 18 claimed, 0 blocked, 117 todo.
**127 / 261 units ported** (83 green, 44 partial), **162,920 / 387,903 upstream lines (42.0%)**. 17 claimed, 0 blocked, 117 todo.
Legend: ✅ green (identical to C++) · 🟡 partial (passes; some cases wait on unported code) · 🔨 claimed · ▶️ ready (all dependencies done) · ⏳ claimable, dependencies still missing · ⛔ blocked.
@@ -138,7 +138,6 @@ Anything `todo` can be claimed. These need stubs for, or co-porting of, the list
- [ ] 🔨 `surface.cone_p1` (1835 lines) — in progress: #31 @bherbruck (ready for review)
- [ ] 🔨 `surface.surf_of_extrusion_p1` (1865 lines) — in progress: #28 @bherbruck (ready for review)
- [ ] 🔨 `surface.surf_of_revolution_p3` (1620 lines) — in progress: #29 @bherbruck (ready for review)
- [ ] 🔨 `surface.torus` (1180 lines) — in progress: #23 @froglemonade2 (ready for review)
- [ ] 🔨 `surface.surf_of_revolution_p2` (1369 lines) — in progress: #51 @froglemonade2 (ready for review)
- [ ] 🔨 `surface.volume_p2` (1490 lines) — in progress: #26 @bherbruck (ready for review)
- [ ] 🔨 `surface.plane_p2` (1627 lines) — in progress: #40 @bherbruck (ready for review)
@@ -265,7 +264,7 @@ Anything `todo` can be claimed. These need stubs for, or co-porting of, the list
- [ ] 🟡 `curve.curve_p5` (2299 lines) — partial, 3097 cases
- [ ] 🔨 `curve.curve_p9` (1783 lines) — in progress: #13 @froglemonade2 (ready for review)
### surface (15/40)
### surface (16/40)
- [ ] 🟡 `surface.volume_p1` (2014 lines) — partial, 2886 cases
- [x] ✅ `surface.b_spline_volume` (1226 lines) — green, 2333 cases
@@ -283,7 +282,7 @@ Anything `todo` can be claimed. These need stubs for, or co-porting of, the list
- [ ] 🔨 `surface.surf_of_extrusion_p1` (1865 lines) — in progress: #28 @bherbruck (ready for review)
- [ ] 🔨 `surface.surf_of_revolution_p3` (1620 lines) — in progress: #29 @bherbruck (ready for review)
- [ ] 🟡 `surface.surface_cache_p2` (1245 lines) — partial, 905 cases
- [ ] 🔨 `surface.torus` (1180 lines) — in progress: #23 @froglemonade2 (ready for review)
- [ ] 🟡 `surface.torus` (1180 lines) — partial, 563 cases
- [ ] 🟡 `surface.unbend_volume` (920 lines) — partial, 1874 cases
- [ ] ⏳ `surface.cone_p2` (1656 lines) — waiting
- [ ] 🔨 `surface.surf_of_revolution_p2` (1369 lines) — in progress: #51 @froglemonade2 (ready for review)
+2 -2
View File
@@ -1705,10 +1705,10 @@ soft_depends_on = ["curve.curve_p2", "geom.geom_utility_p1", "nurbs.b_spline_cur
est_lines = 1180
layer = 24
tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_point", "prog_test:my_test_matrix", "prog_test:my_test_reversed_line_join"]
status = "todo"
status = "partial"
claimed_by = ""
claimed_at = ""
notes = "21 fns; largest: SmTorus::RebuildSTEPFromNURBParameters, SmTorus::CreateOffsetSurface, SmTorus::CreateIsoParametricCurve, SmTorus::EvaluateSTEP; cycle back-edges (call into later units; stub or co-port): surface.surf_of_revolution_p3(2), surface.surf_of_revolution_p2(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit"
notes = "PARTIAL (PR #23): difftest 563 cases (63 fixture: TestSmGeneral torus STEP derivatives, smSurfaceTest analytic torus, smTopoTest torus, SmApiCreateTorus radii, swap/inside-out/partial-domain/explicit-generator edges; + 500 generated) over CreateCanonical, EvaluateSTEP(Point), ConvertUVFromSTEPToNURBS/NURBSToSTEP, CreateIsoParametricCurve, CreateOffsetSurface (+rebuild), RebuildSTEPFromNURBParameters, Transform, operator==, Copy; 545 pass bit-identical, 18 deferred (surface.surf_of_revolution_p3: SmSurfOfRevolution::Transform). ReadFromDB is ported up to SmSurfOfRevolution::ReadFromDB (unported; unit test only). SurfOfRevolution::make_nurb_with_sweep_params_using / rebuild_polar_converter take the iso-curve maker so the torus override is used (upstream virtual CreateIsoParametricCurve). 21 fns; cycle back-edges: surface.surf_of_revolution_p3(2), surface.surf_of_revolution_p2(1)"
[[unit]]
id = "surface.unbend_volume"
File diff suppressed because it is too large Load Diff