WIP: port offset executive part 1 and expose parity blockers

Preserve the safe Rust port, native adapter, fixtures, and differential runner. Native isolated validation: 523 pass, 2 order failures, 17 deferred, 41 rejected and 108 C++ timeouts out of 691 cases. This is not a green or partial correctness-gate claim.

Co-Authored-By: GPT-6.1-Sol <noreply@openai.com>
This commit is contained in:
Alex Lytvynenko
2026-10-17 04:22:57 +02:00
parent ed167cd8ac
commit e173a72239
11 changed files with 13328 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,881 @@
//! Native fixtures for `offset.offset_executive_p1` (PR #175).
//!
//! Upstream: `source/SMLib/inc/SmOffsetExecutive.h`,
//! `source/SMLib/src/SmOffsetExecutive.cpp`.
//!
//! | Rust | Upstream |
//! |------|----------|
//! | [`config`] | constructor/destructor, `Get/SetInset`, `Get/SetExtendConvexEdges`, `SetMergeResults`, `SetCreateOffsetSolid`, `GetTangencyTolDegrees`, `GetOffsetBrep`, `Get/SetShellFaces`, `GetOffsetEdgeuse` |
//! | [`synchronize`] | `SyncronizeOffsetEdgeuses`, indirectly `sm_RemoveSmallStuff`, `sm_InsertBreaks`, `sm_SortEdgeuses`, `sm_ExtractGaps` |
//! | [`extend`] | `GetCreateExtendedSurface` |
//! | [`probe`] | `ReplaceImplicitOffsets`, `RemoveExcessFaces`, `BuildSideWallOfEdgeuse`, `BuildOffsetOfEdgeuses`, `FillGapEdgeuses`, `BuildOffsetOfVertex`, `BuildOffsetOfFaceuse`, `PiecewiseMerge`, `MapEdgeuses`; `sm_NotifyEdgeAndVertices` indirectly via `MapEdgeuses` |
//!
//! Deviations: caller-owned flat snapshots instead of pointers/out-parameters;
//! bounded fixture sizes, and validated NURBS storage before entering C++.
//! A native preflight also rejects two-control-point extension isocurves whose
//! tangent cannot be normalized: upstream's fallback would access a third point
//! outside the polygon (`NL_CrvAdv.cpp`). This returns a shim argument error,
//! never a fixture rejection or a fabricated operation status.
//! No private static helper is copied. Split cases execute native topology splitting;
//! the runner must defer them if its Rust kernel cannot yet split edges.
//!
//! # Geometry constructor views
//!
//! `config` accepts [`Config::tangency_tol_radians`] after `offset_distance` and
//! `approx_tol`, before `operation` in its packed input. Pass `2.0 * PI / 180.0`
//! for the default geometry-constructor view, or any finite radians value for a
//! custom geometry-subclass view. A fixture-only subclass first runs the native
//! default geometry constructor, then assigns its protected tangency scalar;
//! `GetTangencyTolDegrees` remains the actual inherited native getter.
//! Both `default_state` and `final_state` below observe that supplied geometry:
//! "default" refers to executive flags before setters, not a reset of tangency.
//! Other modes keep the unmodified native default geometry constructor.
//!
//! # Snapshot protocol (all entries are `f64`)
//!
//! Every snapshot starts with `0` (fixture built), or consists of `[-1, status]`
//! **only** when a fixture constructor rejected it. Operation failures are native
//! status values inside a built-fixture snapshot, never fixture rejections.
//! All native statuses are **unmodified**: [`NATIVE_SUCCESS`] is 1000, not 0;
//! null-pointer errors are [`NATIVE_NULL_POINTER`] (1006). The leading 0 is an
//! adapter fixture tag, not a native operation status.
//! IDs are zero-based indices in native `GetEdgeuses` / `GetEdges` / `GetFaces`
//! traversal, not addresses. `-1` is null, `-2` is a non-null pointer absent from
//! that traversal. A counted list is `length, entries...`.
//!
//! * `config`: `0, default_state[10], final_state[10], number_shell_sets`, then
//! one counted face-ID list per shell setter, then `number_original_edgeuses`;
//! for each original EU: `GetOffsetEdgeuse_id, counted_related_EU_IDs`;
//! finally a counted list of `RelatePair` statuses (duplicates are passed through).
//! State order: `offset_distance, approx_tol, operation, inset, extend_convex,
//! merge_results, create_solid, tangency_degrees, has_offset_brep, shell_count`.
//! * `synchronize`: `0, operation_status, lookup_first, lookup_second`, counted
//! fixture-primary-EU IDs (first fixtures then second), counted pre-operation
//! relation IDs for first and second, `number_final_edgeuses`, one record per
//! final EU, then four counted EU-ID lists: matched first, matched second,
//! first gaps, second gaps. Lookups/relation IDs use the **pre-operation**
//! traversal; fixture IDs and final lists use the **post-operation** traversal.
//! Synchronization fixtures have real two-edge loops so native splitting has
//! valid CW/CCW neighbors. Auxiliary closing edges are excluded from measured
//! edge/EU traversals; all measured split descendants remain in the snapshot.
//! No UV trims are requested, so arbitrary 3D B-splines remain admissible.
//! Each final EU record is `eu_id, edge_id, min, max, ApproximateLength(10),
//! start_status, start_xyz[3], end_status, end_xyz[3]` (13 entries).
//! * `extend`: `0, action_count`; Get records are `0, status, was_cached,
//! result_id, has_result`. On success append `surface_kind, domain[4],
//! sample_count` and samples `point_status, xyz[3], normal_status, normal[3]`.
//! Domain order is `umin, vmin, umax, vmax`; kind is 1 Plane, 2 B-spline,
//! 3 other. Result IDs are assigned on first successful encounter, never reused
//! after eviction. Forget records: `1, was_cached, remaining_cache_count`;
//! Clear records: `2, previous_cache_count, 0`.
//! * `probe`: `0, status, curve_count, orientation_count, has_surface,
//! has_result_brep, has_offset_brep, counted_original_to_offset_EU_lookups`.
//!
//! **Relation ordering:** this checkout uses pointer-hashed `unordered_set`
//! values in `SmRelation`. We preserve actual `GetSeconds` order; equal-minimum
//! sort ties and summation order can therefore be upstream-nondeterministic.
//! Do not mistake this for insertion order or canonicalize it in the runner.
use crate::{call_with_buffer, FfiError};
/// Fixture resource bound on edges, faces, surfaces, actions and relation pairs.
/// These are adapter budgets, not limits asserted about the kernel API.
pub const MAX_ITEMS: usize = 4096;
/// Maximum number of homogeneous control points in one fixture.
pub const MAX_CONTROL_POINTS: usize = 65_536;
/// Native `NL_MAXDEG` (`NL_Nurbsdef.h`); larger degrees overrun basis work arrays.
pub const MAX_DEGREE: usize = 17;
/// Maximum packed input length, bounding aggregate native fixture allocations.
pub const MAX_PACKED_VALUES: usize = 1_048_576;
/// Aggregate synchronization-edge budget, bounding the quadratic split expansion.
pub const MAX_SYNC_EDGES: usize = 128;
/// Unmodified upstream `SM_SUCCESS` (distinct from the shim ABI's `SC_OK = 0`).
pub const NATIVE_SUCCESS: i32 = 1000;
/// Unmodified upstream `SM_ERR_NULL_POINTER` returned by native NER checks.
pub const NATIVE_NULL_POINTER: i32 = 1006;
/// Constructor settings and protected state exercised by [`config`].
// Independent native setters are boolean toggles, not states of one machine.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
/// Signed offset distance (stored by the geometry creator).
pub offset_distance: f64,
/// Finite approximation tolerance, including the native default sentinel.
pub approx_tol: f64,
/// Geometry-subclass tangency scalar in radians; any finite value is allowed,
/// including signed zero, negative, subnormal and large values. Use
/// `2.0 * PI / 180.0` for the native default constructor's value.
pub tangency_tol_radians: f64,
/// Native operation enum, 0 solid, 1 NMT, 2 shell, 3 local, 4 machine.
pub operation: u32,
/// Value passed to `SetInset`.
pub inset: bool,
/// Value passed to `SetExtendConvexEdges`.
pub extend_convex: bool,
/// Value passed to `SetMergeResults`.
pub merge_results: bool,
/// Value passed to `SetCreateOffsetSolid`.
pub create_solid: bool,
/// Install the fixture offset B-rep into the executive, transferring ownership.
pub install_offset_brep: bool,
/// Number of independent original wire edges (two edgeuses each).
pub original_edges: usize,
/// Number of independent offset wire edges (two edgeuses each).
pub offset_edges: usize,
/// Number of independent planar selector faces in a separate fixture B-rep.
pub face_count: usize,
/// Successive calls to `SetShellFaces`, using native face traversal IDs.
/// Empty outer list leaves the constructor's null shell list untouched.
pub shell_face_sets: Vec<Vec<usize>>,
/// Original-EU/offset-EU traversal IDs; duplicates reach real `RelatePair`.
pub relations: Vec<[usize; 2]>,
}
/// Geometry owned by an independent wire-edge fixture.
#[derive(Debug, Clone, PartialEq)]
pub enum CurveFixture {
/// A real analytic `SmLine`, constructed with `SmLine::CreateLineSegment`.
Line {
/// Point at the lower domain bound.
start: [f64; 3],
/// Point at the upper domain bound; coincident endpoints reach native validation.
end: [f64; 3],
/// Strictly increasing natural parameter interval.
domain: [f64; 2],
},
/// A real `SmBSplineCurve`, not a line stand-in.
BSpline {
/// Polynomial degree, 1 through [`MAX_DEGREE`].
degree: usize,
/// Homogeneous `(x*w, y*w, z*w, w)` control points, positive weights.
control_points: Vec<[f64; 4]>,
/// Nondecreasing knots, length `control_points.len() + degree + 1`.
knots: Vec<f64>,
},
}
/// One independent offset edge and its actual native trimming interval.
#[derive(Debug, Clone, PartialEq)]
pub struct EdgeFixture {
/// Native curve to construct.
pub curve: CurveFixture,
/// Ordered subinterval of the curve's natural interval; zero width is allowed.
pub interval: [f64; 2],
}
/// Arguments for [`synchronize`].
#[derive(Debug, Clone, PartialEq)]
pub struct Synchronize {
/// Mapped offsets of the first original EU, in fixture construction order.
pub first: Vec<EdgeFixture>,
/// Mapped offsets of the other original EU.
pub second: Vec<EdgeFixture>,
/// Pass null as the first original EU: native NER returns its real error code.
pub null_original: bool,
/// Pass null as the other original EU; its otherwise supplied fixtures remain
/// unmapped (the first list can still be nonempty).
pub null_other: bool,
}
/// Surface geometry constructed independently of the Rust kernel's surface types.
#[derive(Debug, Clone, PartialEq)]
pub enum SurfaceFixture {
/// Finite analytic plane with a real B-spline backing representation.
Plane {
/// Parametric origin.
origin: [f64; 3],
/// Native plane X axis (unitized by the constructor).
x_axis: [f64; 3],
/// Native plane Y axis (unitized by the constructor).
y_axis: [f64; 3],
/// Parameter scales; finite, zero and negative scales reach native code.
scale: [f64; 2],
/// `umin, vmin, umax, vmax` (positive extent in both directions).
domain: [f64; 4],
},
/// Tensor-product B-spline surface in U-major control-net order.
BSpline {
/// U degree.
u_degree: usize,
/// V degree.
v_degree: usize,
/// Number of U control points, not the highest index.
u_count: usize,
/// Number of V control points, not the highest index.
v_count: usize,
/// Homogeneous points at `(u, v)`, indexed `u * v_count + v`.
control_points: Vec<[f64; 4]>,
/// U knots, length `u_count + u_degree + 1`.
u_knots: Vec<f64>,
/// V knots, length `v_count + v_degree + 1`.
v_knots: Vec<f64>,
},
}
/// Actions on the executive's actual extended-surface map.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheAction {
/// Call `GetCreateExtendedSurface`; `None` exercises the valid native NER error.
Get(Option<usize>),
/// Remove one map key without deleting any returned surface.
Forget(usize),
/// Clear map entries without deleting returned surfaces.
Clear,
}
/// Arguments for [`extend`].
#[derive(Debug, Clone, PartialEq)]
pub struct Extension {
/// Base surfaces, indexed by [`CacheAction`] keys.
pub surfaces: Vec<SurfaceFixture>,
/// Signed distance; the native method extends by five times this value.
pub offset_distance: f64,
/// Stored approximation tolerance (not used by this extension operation).
pub approx_tol: f64,
/// Repeated gets, eviction and clear operations, executed in order.
pub actions: Vec<CacheAction>,
/// Sample fractions in `[0, 1]^2` of each returned extended natural domain.
pub samples: Vec<[f64; 2]>,
}
/// Independent native topology paths, never implicitly mixed into arithmetic cases.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Probe {
/// `ReplaceImplicitOffsets` on an empty B-rep.
ReplaceEmpty = 0,
/// `RemoveExcessFaces` on an empty B-rep, merging disabled.
RemoveNoMerge = 1,
/// `RemoveExcessFaces` on an empty B-rep, merging enabled.
RemoveEmpty = 2,
/// `BuildSideWallOfEdgeuse(NULL)`.
SideWallNull = 3,
/// `BuildSideWallOfEdgeuse` with a real, unmapped wire EU.
SideWallUnmapped = 4,
/// `FillGapEdgeuses(NULL, empty, empty)`.
FillEmpty = 5,
/// `BuildOffsetOfVertex(NULL, ...)` (native NER).
VertexNull = 6,
/// `BuildOffsetOfVertex` with a real isolated shell vertex.
VertexIsolated = 7,
/// `BuildOffsetOfFaceuse(NULL, ...)` (native NER).
FaceuseNull = 8,
/// Real planar faceuse, null offset surface (native early success).
FaceuseNoSurface = 9,
/// `PiecewiseMerge` on an empty offset B-rep, merge disabled.
PiecewiseEmpty = 10,
/// `MapEdgeuses` from a real planar face's upward use to the same face.
MapPlane = 11,
/// `BuildOffsetOfEdgeuses` on a real planar face edge and its radial mate,
/// with no mapped offsets (no geometry-creator branch is entered).
EdgeusesUnmapped = 12,
}
mod sys {
extern "C" {
pub fn sc_offset_offset_executive_p1_eval(
mode: i32,
input: *const f64,
input_len: i64,
out: *mut f64,
cap: i64,
out_len: *mut i64,
) -> i32;
}
}
fn bad(message: &str) -> FfiError {
FfiError::InvalidArgument(format!("offset.offset_executive_p1: {message}"))
}
fn finite(values: &[f64]) -> Result<(), FfiError> {
if values.iter().all(|v| v.is_finite()) {
Ok(())
} else {
Err(bad("non-finite fixture value"))
}
}
fn count(n: usize) -> Result<(), FfiError> {
if n <= MAX_ITEMS {
Ok(())
} else {
Err(bad("fixture count exceeds MAX_ITEMS"))
}
}
fn interval([a, b]: [f64; 2], positive: bool) -> Result<(), FfiError> {
finite(&[a, b, b - a])?;
if a > b || (positive && a == b) {
return Err(bad("invalid parameter interval"));
}
Ok(())
}
fn knots(degree: usize, n: usize, values: &[f64]) -> Result<[f64; 2], FfiError> {
if !(1..=MAX_DEGREE).contains(&degree)
|| n <= degree
|| n > MAX_CONTROL_POINTS
|| n.checked_add(degree).and_then(|v| v.checked_add(1)) != Some(values.len())
{
return Err(bad("NURBS degree/count/knot length mismatch"));
}
finite(values)?;
if values.windows(2).any(|v| matches!(v, [a, b] if a > b)) {
return Err(bad("decreasing knots"));
}
// Also prevents a zero-width support interval / excessive knot multiplicity.
for (a, b) in values.iter().zip(values.iter().skip(degree + 1)) {
if a >= b || !(b - a).is_finite() {
return Err(bad("zero-width NURBS basis support"));
}
}
let domain = [
*values
.get(degree)
.ok_or_else(|| bad("missing start knot"))?,
*values.get(n).ok_or_else(|| bad("missing end knot"))?,
];
interval(domain, true)?;
Ok(domain)
}
fn points(values: &[[f64; 4]]) -> Result<(), FfiError> {
if values.len() > MAX_CONTROL_POINTS {
return Err(bad("control net exceeds MAX_CONTROL_POINTS"));
}
for &[x, y, z, w] in values {
finite(&[x, y, z, w, x / w, y / w, z / w])?;
if w <= 0.0 {
return Err(bad("fixture homogeneous weights must be positive"));
}
}
Ok(())
}
// Every append checks the aggregate budget before allocating. Counts are derived
// from validated slice lengths, never from a caller-supplied packed header.
#[derive(Default)]
struct Pack(Vec<f64>);
impl Pack {
fn add(&mut self, values: &[f64]) -> Result<(), FfiError> {
if self
.0
.len()
.checked_add(values.len())
.is_none_or(|n| n > MAX_PACKED_VALUES)
{
return Err(bad("aggregate fixture exceeds MAX_PACKED_VALUES"));
}
self.0.extend_from_slice(values);
Ok(())
}
fn size(&mut self, n: usize) -> Result<(), FfiError> {
self.add(&[n as f64])
}
fn control_points(&mut self, values: &[[f64; 4]]) -> Result<(), FfiError> {
points(values)?;
for point in values {
self.add(point)?;
}
Ok(())
}
fn curve(&mut self, curve: &CurveFixture) -> Result<[f64; 2], FfiError> {
match curve {
CurveFixture::Line { start, end, domain } => {
finite(start)?;
finite(end)?;
interval(*domain, true)?;
for (a, b) in start.iter().zip(end) {
finite(&[b - a])?;
}
self.add(&[0.0])?;
self.add(start)?;
self.add(end)?;
self.add(domain)?;
Ok(*domain)
}
CurveFixture::BSpline {
degree,
control_points,
knots: k,
} => {
let domain = knots(*degree, control_points.len(), k)?;
self.add(&[1.0, *degree as f64, control_points.len() as f64])?;
self.control_points(control_points)?;
self.add(k)?;
Ok(domain)
}
}
}
fn edge(&mut self, edge: &EdgeFixture) -> Result<(), FfiError> {
let [a, b] = self.curve(&edge.curve)?;
interval(edge.interval, false)?;
let [min, max] = edge.interval;
if min < a || max > b {
return Err(bad("edge interval is outside the curve domain"));
}
self.add(&edge.interval)
}
fn surface(&mut self, surface: &SurfaceFixture) -> Result<(), FfiError> {
match surface {
SurfaceFixture::Plane {
origin,
x_axis,
y_axis,
scale,
domain,
} => {
finite(origin)?;
finite(x_axis)?;
finite(y_axis)?;
finite(scale)?;
let [a, b, c, d] = *domain;
interval([a, c], true)?;
interval([b, d], true)?;
// Zero axes are not rejected: native Unitize has a defined fallback.
self.add(&[0.0])?;
self.add(origin)?;
self.add(x_axis)?;
self.add(y_axis)?;
self.add(scale)?;
self.add(domain)
}
SurfaceFixture::BSpline {
u_degree,
v_degree,
u_count,
v_count,
control_points,
u_knots,
v_knots,
} => {
if u_count.checked_mul(*v_count) != Some(control_points.len()) {
return Err(bad("surface control net length mismatch/overflow"));
}
knots(*u_degree, *u_count, u_knots)?;
knots(*v_degree, *v_count, v_knots)?;
self.add(&[
1.0,
*u_degree as f64,
*v_degree as f64,
*u_count as f64,
*v_count as f64,
])?;
self.control_points(control_points)?;
self.add(u_knots)?;
self.add(v_knots)
}
}
}
}
fn config_input(input: &Config) -> Result<Pack, FfiError> {
finite(&[
input.offset_distance,
input.approx_tol,
input.tangency_tol_radians,
])?;
if input.operation > 4 {
return Err(bad("invalid offset operation"));
}
for n in [
input.original_edges,
input.offset_edges,
input.face_count,
input.shell_face_sets.len(),
input.relations.len(),
] {
count(n)?;
}
let mut pack = Pack::default();
pack.add(&[
input.offset_distance,
input.approx_tol,
input.tangency_tol_radians,
f64::from(input.operation),
f64::from(input.inset),
f64::from(input.extend_convex),
f64::from(input.merge_results),
f64::from(input.create_solid),
f64::from(input.install_offset_brep),
input.original_edges as f64,
input.offset_edges as f64,
input.face_count as f64,
])?;
pack.size(input.shell_face_sets.len())?;
for selectors in &input.shell_face_sets {
count(selectors.len())?;
pack.size(selectors.len())?;
for &id in selectors {
if id >= input.face_count {
return Err(bad("shell face selector out of range"));
}
pack.size(id)?;
}
}
pack.size(input.relations.len())?;
for &[a, b] in &input.relations {
if a >= input.original_edges * 2 || b >= input.offset_edges * 2 {
return Err(bad("relation EU selector out of range"));
}
pack.add(&[a as f64, b as f64])?;
}
Ok(pack)
}
fn sync_input(input: &Synchronize) -> Result<Pack, FfiError> {
count(input.first.len())?;
count(input.second.len())?;
if input
.first
.len()
.checked_add(input.second.len())
.is_none_or(|n| n > MAX_SYNC_EDGES)
{
return Err(bad("synchronization exceeds MAX_SYNC_EDGES"));
}
let mut pack = Pack::default();
pack.add(&[f64::from(input.null_original), f64::from(input.null_other)])?;
for edges in [&input.first, &input.second] {
pack.size(edges.len())?;
for edge in edges {
pack.edge(edge)?;
}
}
Ok(pack)
}
fn extension_input(input: &Extension) -> Result<Pack, FfiError> {
finite(&[
input.offset_distance,
input.offset_distance * 5.0,
input.approx_tol,
])?;
for n in [
input.surfaces.len(),
input.actions.len(),
input.samples.len(),
] {
count(n)?;
}
let gets = input
.actions
.iter()
.filter(|a| matches!(a, CacheAction::Get(_)))
.count();
let output_bound = input
.samples
.len()
.checked_mul(8)
.and_then(|n| n.checked_add(11))
.and_then(|n| n.checked_mul(gets))
.and_then(|n| n.checked_add(input.actions.len() * 3 + 2));
if output_bound.is_none_or(|n| n > MAX_PACKED_VALUES) {
return Err(bad("extension snapshot exceeds MAX_PACKED_VALUES"));
}
let mut pack = Pack::default();
pack.add(&[input.offset_distance, input.approx_tol])?;
pack.size(input.surfaces.len())?;
for surface in &input.surfaces {
pack.surface(surface)?;
}
pack.size(input.samples.len())?;
for sample in &input.samples {
finite(sample)?;
if sample.iter().any(|v| !(0.0..=1.0).contains(v)) {
return Err(bad("surface sample fractions must be in [0, 1]"));
}
pack.add(sample)?;
}
pack.size(input.actions.len())?;
for action in &input.actions {
match *action {
CacheAction::Get(key) => {
if let Some(key) = key {
if key >= input.surfaces.len() {
return Err(bad("surface key out of range"));
}
}
pack.add(&[0.0, key.map_or(-1.0, |k| k as f64)])?;
}
CacheAction::Forget(key) => {
if key >= input.surfaces.len() {
return Err(bad("surface key out of range"));
}
pack.add(&[1.0, key as f64])?;
}
CacheAction::Clear => pack.add(&[2.0])?,
}
}
Ok(pack)
}
fn run(mode: i32, pack: &Pack) -> Result<Vec<f64>, FfiError> {
call_with_buffer(1024, |out, len| {
// SAFETY: only the typed, validated packers call this private function.
// All counts, knot layouts, selectors and scalar finiteness were checked;
// input/output slices and len remain live and C++ honors out's capacity.
unsafe {
sys::sc_offset_offset_executive_p1_eval(
mode,
pack.0.as_ptr(),
pack.0.len() as i64,
out.as_mut_ptr(),
out.len() as i64,
&raw mut *len,
)
}
})
}
/// Constructs real native topology and snapshots executive state and relationships.
/// See the module-level protocol for the returned flat record.
pub fn config(input: &Config) -> Result<Vec<f64>, FfiError> {
run(0, &config_input(input)?)
}
/// Synchronizes actual line/B-spline edgeuses through the native public method.
/// Native split-path statuses/results are retained even when the Rust side defers.
pub fn synchronize(input: &Synchronize) -> Result<Vec<f64>, FfiError> {
run(1, &sync_input(input)?)
}
/// Exercises real surface extension, cache hits/eviction and evaluated samples.
/// Native operation/evaluation errors remain in the snapshot.
pub fn extend(input: &Extension) -> Result<Vec<f64>, FfiError> {
run(2, &extension_input(input)?)
}
/// Runs one independent topology probe. The tolerance is forwarded unchanged;
/// only non-finite values are rejected before calling native code.
pub fn probe(operation: Probe, approx_tol: f64) -> Result<Vec<f64>, FfiError> {
finite(&[approx_tol])?;
run(3, &Pack(vec![f64::from(operation as i32), approx_tol]))
}
#[cfg(test)]
mod tests {
use super::*;
fn line(interval: [f64; 2]) -> EdgeFixture {
EdgeFixture {
curve: CurveFixture::Line {
start: [0.0, 0.0, 0.0],
end: [1.0, 0.0, 0.0],
domain: [0.0, 1.0],
},
interval,
}
}
#[test]
fn sync_accepts_gaps_zero_width_and_nulls() {
assert!(sync_input(&Synchronize {
first: vec![line([0.8, 1.0]), line([0.0, 0.0]), line([0.0, 0.2])],
second: vec![],
null_original: true,
null_other: true,
})
.is_ok());
}
#[test]
fn sync_rejects_bad_ranges_and_nonfinite() {
for range in [[0.5, 0.4], [-0.1, 0.5], [0.0, 1.1], [0.0, f64::NAN]] {
assert!(sync_input(&Synchronize {
first: vec![line(range)],
second: vec![],
null_original: false,
null_other: false,
})
.is_err());
}
}
#[test]
fn validates_native_nurbs_storage() {
assert!(knots(1, 2, &[0.0, 0.0, 1.0, 1.0]).is_ok());
assert!(knots(MAX_DEGREE + 1, 32, &[]).is_err());
assert!(knots(1, 2, &[0.0, 0.0, 0.0, 1.0]).is_err());
assert!(knots(2, 3, &[0.0, 0.0, 0.0, 1.0, 0.5, 1.0]).is_err());
assert!(points(&[[0.0, 0.0, 0.0, 0.0]]).is_err());
assert!(points(&[[f64::INFINITY, 0.0, 0.0, 1.0]]).is_err());
assert!(points(&[[2.0, 0.0, 0.0, 2.0]]).is_ok());
}
#[test]
fn extension_checks_net_overflow_keys_and_samples() {
let surface = SurfaceFixture::BSpline {
u_degree: 1,
v_degree: 1,
u_count: usize::MAX,
v_count: 2,
control_points: vec![],
u_knots: vec![],
v_knots: vec![],
};
assert!(Pack::default().surface(&surface).is_err());
let mut ext = Extension {
surfaces: vec![],
offset_distance: 0.5,
approx_tol: 1.0e-6,
actions: vec![CacheAction::Get(None), CacheAction::Clear],
samples: vec![],
};
assert!(extension_input(&ext).is_ok());
ext.actions.push(CacheAction::Get(Some(0)));
assert!(extension_input(&ext).is_err());
ext.actions.clear();
ext.samples.push([1.01, 0.0]);
assert!(extension_input(&ext).is_err());
}
#[test]
fn config_checks_selectors_not_duplicate_relations() {
let mut cfg = Config {
offset_distance: -0.2,
approx_tol: 0.0,
tangency_tol_radians: 2.0 * std::f64::consts::PI / 180.0,
operation: 4,
inset: false,
extend_convex: false,
merge_results: false,
create_solid: false,
install_offset_brep: true,
original_edges: 1,
offset_edges: 1,
face_count: 1,
shell_face_sets: vec![vec![0, 0], vec![]],
relations: vec![[0, 1], [0, 1]],
};
assert!(config_input(&cfg).is_ok());
cfg.relations.push([2, 0]);
assert!(config_input(&cfg).is_err());
cfg.relations.clear();
cfg.shell_face_sets.push(vec![1]);
assert!(config_input(&cfg).is_err());
}
#[test]
fn config_validates_tangency_finiteness_only_and_packs_before_operation() {
let mut cfg = Config {
offset_distance: 0.25,
approx_tol: 1.0e-6,
tangency_tol_radians: 2.0 * std::f64::consts::PI / 180.0,
operation: 4,
inset: false,
extend_convex: false,
merge_results: false,
create_solid: false,
install_offset_brep: false,
original_edges: 0,
offset_edges: 0,
face_count: 0,
shell_face_sets: vec![],
relations: vec![],
};
for tangency in [
cfg.tangency_tol_radians,
0.0,
-0.0,
-0.25,
f64::from_bits(1),
f64::MIN_POSITIVE,
f64::MIN,
f64::MAX,
] {
cfg.tangency_tol_radians = tangency;
let packed = config_input(&cfg).unwrap();
assert_eq!(packed.0.len(), 14);
assert_eq!(packed.0.first(), Some(&cfg.offset_distance));
assert_eq!(packed.0.get(1), Some(&cfg.approx_tol));
assert_eq!(
packed.0.get(2).map(|v| v.to_bits()),
Some(tangency.to_bits())
);
assert_eq!(packed.0.get(3), Some(&f64::from(cfg.operation)));
}
for tangency in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
cfg.tangency_tol_radians = tangency;
assert!(matches!(
config_input(&cfg),
Err(FfiError::InvalidArgument(_))
));
}
}
#[test]
fn synchronization_and_snapshot_growth_are_bounded() {
assert!(sync_input(&Synchronize {
first: vec![line([0.0, 1.0]); MAX_SYNC_EDGES + 1],
second: vec![],
null_original: false,
null_other: false,
})
.is_err());
assert!(extension_input(&Extension {
surfaces: vec![],
offset_distance: 1.0,
approx_tol: 0.0,
actions: vec![CacheAction::Get(None); MAX_ITEMS],
samples: vec![[0.5, 0.5]; MAX_ITEMS],
})
.is_err());
}
#[test]
fn accepts_real_bspline_and_plane_layouts() {
let curve = CurveFixture::BSpline {
degree: 2,
control_points: vec![
[0.0, 0.0, 0.0, 1.0],
[0.5, 1.0, 0.0, 1.0],
[1.0, 0.0, 0.0, 1.0],
],
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
};
assert!(sync_input(&Synchronize {
first: vec![EdgeFixture {
curve,
interval: [0.2, 0.8]
}],
second: vec![],
null_original: false,
null_other: false,
})
.is_ok());
assert!(extension_input(&Extension {
surfaces: vec![SurfaceFixture::Plane {
origin: [0.0, 0.0, 0.0],
x_axis: [1.0, 0.0, 0.0],
y_axis: [0.0, 1.0, 0.0],
scale: [2.0, -3.0],
domain: [0.0, 0.0, 1.0, 1.0],
}],
offset_distance: 0.25,
approx_tol: 1.0e-6,
actions: vec![
CacheAction::Get(Some(0)),
CacheAction::Forget(0),
CacheAction::Clear
],
samples: vec![[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]],
})
.is_ok());
}
#[test]
fn aggregate_budget_is_checked_before_append() {
let mut pack = Pack(vec![0.0; MAX_PACKED_VALUES]);
assert!(pack.add(&[1.0]).is_err());
assert_eq!(pack.0.len(), MAX_PACKED_VALUES);
}
}
+2
View File
@@ -3,3 +3,5 @@
//! Upstream: `SmOffsetExecutive`, `SmOffsetGeometryCreation`, `SmOffsetSurface`,
//! `SmOffsetCurve` in `source/SMLib`, and the offset/shell entry points of
//! `SM_API/SmApiBrep`.
pub mod offset_executive;
@@ -0,0 +1,12 @@
//! Topological offset executive, split by callgraph ownership.
//!
//! Upstream: `source/SMLib/inc/SmOffsetExecutive.h`,
//! `source/SMLib/src/SmOffsetExecutive.cpp`.
//!
//! | Rust | Upstream |
//! |------|----------|
//! | [`p1::OffsetExecutive`] | `SmOffsetExecutive` |
//!
//! Deviations: typed arena ids replace pointers; each part documents its dependency boundaries.
pub mod p1;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,332 @@
{
"cases": [
{
"name": "operation_0",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": false,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
},
{
"name": "operation_1",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 1,
"inset": true,
"extend_convex": false,
"merge_results": true,
"create_solid": false,
"install_offset_brep": true,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
},
{
"name": "operation_2",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 2,
"inset": false,
"extend_convex": true,
"merge_results": false,
"create_solid": true,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
},
{
"name": "operation_3",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 3,
"inset": true,
"extend_convex": true,
"merge_results": true,
"create_solid": true,
"install_offset_brep": true,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
},
{
"name": "operation_4",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 4,
"inset": false,
"extend_convex": false,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
},
{
"name": "upstream_prog_offset_setters",
"inputs": {
"op": "config",
"offset_distance": 0.125,
"approx_tol": 1e-06,
"operation": 0,
"inset": true,
"extend_convex": false,
"merge_results": true,
"create_solid": false,
"install_offset_brep": true,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [
[
0
]
],
"relations": []
},
"upstream_ref": "tests/prog_test/src/smOffsetTest.cpp:58-71",
"note": "Re-expresses the executive settings prefix, not DoSolidOffset or its file input."
},
{
"name": "upstream_shell_duplicates_and_replacement",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": true,
"merge_results": true,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [
[
0,
0
],
[
2,
1,
2,
0
],
[],
[
1,
1
]
],
"relations": []
},
"upstream_ref": "tests/SM_API_test/src/TestSmBrep.cpp:2619-2638",
"note": "Duplicate shell selectors from TestSmShellBrepFull; extends coverage to repeated SetShellFaces replacement."
},
{
"name": "upstream_offset_settings_prefix",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": true,
"merge_results": true,
"create_solid": true,
"install_offset_brep": true,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
},
"upstream_ref": "tests/SM_API_test/src/TestSmBrep.cpp:2745-2755",
"note": "Settings prefix inspired by the box offset entry point; this is not a complete box offset fixture."
},
{
"name": "duplicate_pairs_exact_one_and_many",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": false,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": [
[
0,
0
],
[
0,
0
],
[
0,
2
],
[
1,
1
],
[
1,
1
],
[
2,
1
],
[
2,
1
],
[
3,
5
]
]
}
},
{
"name": "empty_arenas_and_empty_shell_replacement",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": false,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 0,
"offset_edges": 0,
"face_count": 0,
"shell_face_sets": [
[],
[]
],
"relations": []
}
},
{
"name": "config_positive_zero",
"inputs": {
"op": "config",
"offset_distance": 0.0,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": false,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
},
{
"name": "config_negative_zero",
"inputs": {
"op": "config",
"offset_distance": -0.0,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": false,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
},
{
"name": "config_tiny_distance",
"inputs": {
"op": "config",
"offset_distance": 1e-12,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": false,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
},
{
"name": "config_huge_negative_distance",
"inputs": {
"op": "config",
"offset_distance": -1000000000.0,
"approx_tol": 1e-06,
"operation": 0,
"inset": false,
"extend_convex": false,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 2,
"offset_edges": 3,
"face_count": 3,
"shell_face_sets": [],
"relations": []
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
{
"cases": [
{
"name": "probe_replace_empty",
"inputs": {
"op": "probe",
"branch": "replace_empty",
"approx_tol": 1e-06
}
},
{
"name": "probe_remove_no_merge",
"inputs": {
"op": "probe",
"branch": "remove_no_merge",
"approx_tol": 1e-06
}
},
{
"name": "probe_remove_empty",
"inputs": {
"op": "probe",
"branch": "remove_empty",
"approx_tol": 1e-06
}
},
{
"name": "probe_side_wall_null",
"inputs": {
"op": "probe",
"branch": "side_wall_null",
"approx_tol": 1e-06
}
},
{
"name": "probe_side_wall_unmapped",
"inputs": {
"op": "probe",
"branch": "side_wall_unmapped",
"approx_tol": 1e-06
}
},
{
"name": "probe_fill_empty",
"inputs": {
"op": "probe",
"branch": "fill_empty",
"approx_tol": 1e-06
}
},
{
"name": "probe_vertex_null",
"inputs": {
"op": "probe",
"branch": "vertex_null",
"approx_tol": 1e-06
}
},
{
"name": "probe_vertex_isolated",
"inputs": {
"op": "probe",
"branch": "vertex_isolated",
"approx_tol": 1e-06
}
},
{
"name": "probe_faceuse_null",
"inputs": {
"op": "probe",
"branch": "faceuse_null",
"approx_tol": 1e-06
}
},
{
"name": "probe_faceuse_no_surface",
"inputs": {
"op": "probe",
"branch": "faceuse_no_surface",
"approx_tol": 1e-06
}
},
{
"name": "probe_piecewise_empty",
"inputs": {
"op": "probe",
"branch": "piecewise_empty",
"approx_tol": 1e-06
}
},
{
"name": "probe_map_plane",
"inputs": {
"op": "probe",
"branch": "map_plane",
"approx_tol": 1e-06
}
},
{
"name": "probe_edgeuses_unmapped",
"inputs": {
"op": "probe",
"branch": "edgeuses_unmapped",
"approx_tol": 1e-06
}
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,166 @@
{
"cases": [
{
"name": "config_tangency_positive_zero",
"inputs": {
"op": "config",
"offset_distance": 0.125,
"approx_tol": 1e-6,
"tangency_tol_radians": 0.0,
"operation": 0,
"inset": false,
"extend_convex": true,
"merge_results": true,
"create_solid": false,
"install_offset_brep": true,
"original_edges": 1,
"offset_edges": 2,
"face_count": 2,
"shell_face_sets": [[1, 1], [0, 1, 0], []],
"relations": [[0, 0], [0, 0], [1, 2], [1, 3]]
}
},
{
"name": "config_tangency_negative_zero",
"inputs": {
"op": "config",
"offset_distance": -0.125,
"approx_tol": 1e-6,
"tangency_tol_radians": -0.0,
"operation": 1,
"inset": true,
"extend_convex": false,
"merge_results": false,
"create_solid": true,
"install_offset_brep": false,
"original_edges": 1,
"offset_edges": 2,
"face_count": 2,
"shell_face_sets": [[1, 1], [0, 1, 0], []],
"relations": [[0, 0], [0, 0], [1, 2], [1, 3]]
}
},
{
"name": "config_tangency_tiny_positive",
"inputs": {
"op": "config",
"offset_distance": 0.125,
"approx_tol": 1e-12,
"tangency_tol_radians": 1e-12,
"operation": 2,
"inset": false,
"extend_convex": true,
"merge_results": true,
"create_solid": true,
"install_offset_brep": true,
"original_edges": 1,
"offset_edges": 2,
"face_count": 2,
"shell_face_sets": [[1, 1], [0, 1, 0], []],
"relations": [[0, 0], [0, 0], [1, 2], [1, 3]]
}
},
{
"name": "config_tangency_tiny_negative",
"inputs": {
"op": "config",
"offset_distance": -0.125,
"approx_tol": 1e-12,
"tangency_tol_radians": -1e-12,
"operation": 3,
"inset": true,
"extend_convex": true,
"merge_results": false,
"create_solid": false,
"install_offset_brep": false,
"original_edges": 1,
"offset_edges": 2,
"face_count": 2,
"shell_face_sets": [[1, 1], [0, 1, 0], []],
"relations": [[0, 0], [0, 0], [1, 2], [1, 3]]
}
},
{
"name": "config_tangency_positive_pi",
"inputs": {
"op": "config",
"offset_distance": 1.0,
"approx_tol": 1e-6,
"tangency_tol_radians": 3.141592653589793,
"operation": 4,
"inset": false,
"extend_convex": false,
"merge_results": true,
"create_solid": false,
"install_offset_brep": true,
"original_edges": 1,
"offset_edges": 2,
"face_count": 2,
"shell_face_sets": [[1, 1], [0, 1, 0], []],
"relations": [[0, 0], [0, 0], [1, 2], [1, 3]]
}
},
{
"name": "config_tangency_negative_pi",
"inputs": {
"op": "config",
"offset_distance": -1.0,
"approx_tol": 1e-6,
"tangency_tol_radians": -3.141592653589793,
"operation": 0,
"inset": true,
"extend_convex": false,
"merge_results": true,
"create_solid": true,
"install_offset_brep": false,
"original_edges": 1,
"offset_edges": 2,
"face_count": 2,
"shell_face_sets": [[1, 1], [0, 1, 0], []],
"relations": [[0, 0], [0, 0], [1, 2], [1, 3]]
}
},
{
"name": "config_tangency_nonround_positive_order_sensitive",
"note": "Compute radians * 180.0 / PI in that order, not radians * (180.0 / PI).",
"inputs": {
"op": "config",
"offset_distance": 0.125,
"approx_tol": 1e-6,
"tangency_tol_radians": 0.1234567890123456,
"operation": 1,
"inset": false,
"extend_convex": true,
"merge_results": false,
"create_solid": true,
"install_offset_brep": true,
"original_edges": 1,
"offset_edges": 2,
"face_count": 2,
"shell_face_sets": [[1, 1], [0, 1, 0], []],
"relations": [[0, 0], [0, 0], [1, 2], [1, 3]]
}
},
{
"name": "config_tangency_nonround_negative_order_sensitive",
"note": "The custom geometry tolerance remains signed in both constructor and final snapshots.",
"inputs": {
"op": "config",
"offset_distance": -0.125,
"approx_tol": 1e-6,
"tangency_tol_radians": -0.1234567890123456,
"operation": 2,
"inset": true,
"extend_convex": true,
"merge_results": true,
"create_solid": true,
"install_offset_brep": false,
"original_edges": 1,
"offset_edges": 2,
"face_count": 2,
"shell_face_sets": [[1, 1], [0, 1, 0], []],
"relations": [[0, 0], [0, 0], [1, 2], [1, 3]]
}
}
]
}
@@ -0,0 +1,652 @@
// SPDX-License-Identifier: Apache-2.0
// Native adapter for offset.offset_executive_p1 (PR #175).
// Upstream: source/SMLib/inc/SmOffsetExecutive.h, src/SmOffsetExecutive.cpp.
//
// Rust API/protocol: crates/solidcore-ffi/src/units/offset_offset_executive_p1.rs.
// config -> default/custom geometry ctor views, native executive ctor/dtor,
// state setters/getters, SmRelation/GetOffsetEdgeuse.
// Config input begins distance, approx_tol, tangency_tol_radians, operation.
// Its constructor-state and final-state snapshots both use the supplied geometry;
// other modes retain the native default 2-degree tangency constructor behavior.
// synchronize -> SyncronizeOffsetEdgeuses, which calls all four owned static helpers.
// extend -> GetCreateExtendedSurface with real SmPlane/SmBSplineSurface geometry.
// probe -> independent safe topology paths; not a replacement for arithmetic tests.
// Deviations: bounded plain-C fixture protocol, traversal IDs instead of pointers.
// No implementation file is included; no native numerical/helper code is copied.
#include "sc_shim.h"
#include "StdAfx.h"
#include <SmBSplineCurve.h>
#include <SmBSplineSurface.h>
#include <SmBrep.h>
#include <SmContext.h>
#include <SmEdge.h>
#include <SmEdgeuse.h>
#include <SmFace.h>
#include <SmFaceuse.h>
#include <SmLine.h>
#include <SmNurbsCrv.h>
#include <SmNurbsSrf.h>
#include <SmOffsetExecutive.h>
#include <SmOffsetGeometryCreation.h>
#include <SmPlane.h>
#include <SmSSIData.h>
#include <SmVertex.h>
#include <nurbs.h>
#include <algorithm>
#include <cmath>
#include <memory>
#include <new>
#include <vector>
namespace {
constexpr int64_t max_values = 1048576;
constexpr int64_t max_items = 4096;
constexpr int64_t max_points = 65536;
constexpr int64_t max_degree = 17; // NL_MAXDEG, not an arbitrary evaluation degree.
struct Argument {};
struct Rejected { SmStatus status; };
// A second bounds check here makes malformed direct ABI calls fail as arguments,
// not read beyond the caller's buffer. Geometric invariants are checked in Rust.
struct Reader {
const double* data;
int64_t size;
int64_t pos = 0;
double number() {
if (pos >= size) throw Argument{};
double v = data[pos++];
if (!std::isfinite(v)) throw Argument{};
return v;
}
int64_t index(int64_t limit = max_items) {
double v = number();
if (v < 0 || v > (double)limit || std::floor(v) != v) throw Argument{};
return (int64_t)v;
}
bool flag() { return index(1) != 0; }
SmPoint3d point() {
double x = number(), y = number(), z = number();
return SmPoint3d(x, y, z);
}
SmPoint2d uv() {
double x = number(), y = number();
return SmPoint2d(x, y);
}
SmExtent1d interval() {
double a = number(), b = number();
if (a > b || !std::isfinite(b-a)) throw Argument{};
return SmExtent1d(a, b);
}
SmExtent2d domain() {
SmPoint2d a = uv(), b = uv();
if (!(a.x < b.x && a.y < b.y)) throw Argument{};
return SmExtent2d(a, b);
}
void done() const { if (pos != size) throw Argument{}; }
};
void constructed(SmStatus status) {
if (status != SM_SUCCESS) throw Rejected{status};
}
void put_point(std::vector<double>& out, const SmPoint3d& p) {
out.insert(out.end(), {p.x, p.y, p.z});
}
void put_domain(std::vector<double>& out, const SmExtent2d& d) {
out.insert(out.end(), {d.GetMin().x, d.GetMin().y, d.GetMax().x, d.GetMax().y});
}
template<class T> double id(const SmTArray<T*>& list, const T* p) {
if (!p) return -1;
for (ULONG i=0; i<list.GetSize(); ++i) if (list[i] == p) return (double)i;
return -2;
}
void put_ids(std::vector<double>& out, const SmTArray<SmEdgeuse*>& values,
const SmTArray<SmEdgeuse*>& traversal) {
out.push_back(values.GetSize());
for (ULONG i=0; i<values.GetSize(); ++i) out.push_back(id(traversal, values[i]));
}
std::unique_ptr<SmBrep> brep(SmContext& ctx) {
return std::unique_ptr<SmBrep>(new (ctx) SmBrep(SM_USE_DEFAULT, SM_USE_DEFAULT, &ctx));
}
// Config-only geometry-subclass fixture. Preserve base initialization and the
// inherited GetTangencyTolDegrees calculation; only the protected scalar changes.
class ConfigGeometry : public SmOffsetGeometryCreation {
public:
ConfigGeometry(double distance, double tol, double tangency_tol_radians)
: SmOffsetGeometryCreation(distance, tol) {
m_dTangencyTolRadians = tangency_tol_radians;
}
};
// Only fixture injection/observation: all behavior under test remains native.
class Executive : public SmOffsetExecutive {
public:
Executive(SmContext& ctx, SmOffsetOperationType op, SmOffsetGeometryCreation& geometry,
SmBrep* original) : SmOffsetExecutive(ctx, op, geometry, original) {}
void install(std::unique_ptr<SmBrep> offset) { m_pOffsetBrep = offset.release(); }
SmStatus relate(SmEdgeuse* a, SmEdgeuse* b) { return m_vEUToEU.RelatePair(a, b); }
void seconds(SmEdgeuse* a, SmTArray<SmEdgeuse*>& b) { m_vEUToEU.GetSeconds(a, b); }
bool merge() const { return m_bMergeResults != FALSE; }
bool solid() const { return m_bCreateOffsetSolid != FALSE; }
SmSurface* cached(SmSurface* base) { return m_vSurfExtSurf.At(base); }
void forget(SmSurface* base) { m_vSurfExtSurf.Remove(base); }
void clear() { m_vSurfExtSurf.RemoveAll(); }
ULONG cache_count() const { return m_vSurfExtSurf.Count(); }
};
std::unique_ptr<SmCurve> read_curve(Reader& rd, SmContext& ctx) {
int64_t kind = rd.index(1);
if (kind == 0) {
SmPoint3d start = rd.point(), end = rd.point();
SmExtent1d domain = rd.interval();
SmLine* line = nullptr;
SmStatus status = SmLine::CreateLineSegment(ctx, 3, start, end, line, &domain);
std::unique_ptr<SmCurve> result(line);
constructed(status);
if (!result) throw std::bad_alloc();
return result;
}
auto degree = (gw_DEGREE)rd.index(max_degree);
auto npoints = (gw_INDEX)rd.index(max_points);
if (degree < 1 || npoints <= degree) throw Argument{};
gw_INDEX n = npoints - 1, m = npoints + degree;
std::unique_ptr<gw_CURVE, decltype(&sm_FreeNurbCurve)> raw(
sm_AllocateNurbCurve(n, degree, m), &sm_FreeNurbCurve);
gw_CURVE* curve = raw.get();
if (!curve) throw std::bad_alloc();
// The native constructor tunes knots, so initialize storage BEFORE invoking it.
for (gw_INDEX i=0; i<npoints; ++i) {
curve->pol->Pw[i].x = rd.number(); curve->pol->Pw[i].y = rd.number();
curve->pol->Pw[i].z = rd.number(); curve->pol->Pw[i].w = rd.number();
}
for (gw_INDEX i=0; i<=m; ++i) curve->knt->U[i] = rd.number();
std::unique_ptr<SmCurve> result(new (ctx) SmBSplineCurve(curve, 3, FALSE, &ctx));
raw.release();
return result;
}
SmEdge* make_wire(SmContext& ctx, SmBrep* target, std::unique_ptr<SmCurve> curve,
const SmExtent1d& interval) {
SmPoint3d a(0,0,0), b(0,0,0);
constructed(curve->EvaluatePoint(interval.GetMin(), a));
constructed(curve->EvaluatePoint(interval.GetMax(), b));
SmShell* shell = nullptr;
SmVertex *v1 = nullptr, *v2 = nullptr;
constructed(target->MakeShellVertex(nullptr, a, shell, v1));
constructed(target->MakeShellVertex(nullptr, b, shell, v2));
SmEdge* edge = nullptr;
// MakeWireEdge can fail after assigning the curve to a newly made edge.
// The guard must not delete a curve already owned by that topology.
struct Pending {
SmCurve* p;
~Pending() { if (p && !p->GetOwner()) delete p; }
} pending{curve.release()};
constructed(target->MakeWireEdge(nullptr, v1, v2, pending.p, interval, SM_OT_SAME, edge));
if (!edge || !edge->GetPrimaryEdgeuse()) throw Argument{};
(void)ctx;
return edge;
}
SmEdge* unit_wire(SmContext& ctx, SmBrep* target, double y) {
SmLine* curve = nullptr;
SmExtent1d interval(0,1);
constructed(SmLine::CreateLineSegment(ctx, 3, SmPoint3d(0,y,0), SmPoint3d(1,y,0),
curve, &interval));
return make_wire(ctx, target, std::unique_ptr<SmCurve>(curve), interval);
}
SmFace* plane_face(SmContext& ctx, SmBrep* target, double z) {
SmExtent2d domain(SmPoint2d(0,0), SmPoint2d(1,1));
SmPlane* plane = new (ctx) SmPlane(SmPoint3d(0,0,z), SmVector3d(1,0,0),
SmVector3d(0,1,0), SmVector2d(1,1), domain, &ctx);
struct Pending {
SmSurface* p;
~Pending() { if (p && !p->GetOwner()) delete p; }
} pending{plane};
SmFace* face = nullptr;
constructed(target->CreateFaceFromSurface(plane, domain, face));
if (!face) throw Argument{};
return face;
}
std::unique_ptr<SmSurface> read_surface(Reader& rd, SmContext& ctx) {
int64_t kind = rd.index(1);
if (kind == 0) {
SmPoint3d origin = rd.point();
SmVector3d x = rd.point(), y = rd.point();
SmVector2d scale = rd.uv();
SmExtent2d domain = rd.domain();
return std::unique_ptr<SmSurface>(new (ctx) SmPlane(origin, x, y, scale, domain, &ctx));
}
auto p = (gw_DEGREE)rd.index(max_degree), q = (gw_DEGREE)rd.index(max_degree);
auto nu = (gw_INDEX)rd.index(max_points), nv = (gw_INDEX)rd.index(max_points);
if (p < 1 || q < 1 || nu <= p || nv <= q || (int64_t)nu*nv > max_points)
throw Argument{};
gw_INDEX r = nu+p, s = nv+q;
auto free_surface = [](gw_SURFACE* p) { smos_Free(p); };
std::unique_ptr<gw_SURFACE, decltype(free_surface)> raw(
sm_AllocateNurbSurface(nu-1, nv-1, p, q, r, s), free_surface);
gw_SURFACE* surface = raw.get();
if (!surface) throw std::bad_alloc();
for (gw_INDEX i=0; i<nu; ++i) for (gw_INDEX j=0; j<nv; ++j) {
surface->net->Pw[i][j].x = rd.number(); surface->net->Pw[i][j].y = rd.number();
surface->net->Pw[i][j].z = rd.number(); surface->net->Pw[i][j].w = rd.number();
}
for (gw_INDEX i=0; i<=r; ++i) surface->knu->U[i] = rd.number();
for (gw_INDEX i=0; i<=s; ++i) surface->knv->U[i] = rd.number();
std::unique_ptr<SmBSplineSurface> result(new (ctx) SmBSplineSurface(surface, FALSE, &ctx));
raw.release();
result->SetBSplineSurfaceForm(SM_SF_UNSPECIFIED);
return std::unique_ptr<SmSurface>(result.release());
}
void state(std::vector<double>& out, Executive& executive, SmOffsetGeometryCreation& geometry) {
SmTArray<SmFace*> faces;
executive.GetShellFaces(faces);
out.insert(out.end(), {
geometry.GetOffsetDistance(), geometry.GetThisApproxTol3d(),
(double)executive.GetOffsetOperation(), (double)executive.GetInset(),
(double)executive.GetExtendConvexEdges(), (double)executive.merge(),
(double)executive.solid(), executive.GetTangencyTolDegrees(),
executive.GetOffsetBrep() ? 1.0 : 0.0, (double)faces.GetSize()
});
}
void config(Reader& rd, std::vector<double>& out) {
double distance = rd.number(), tol = rd.number(), tangency_tol_radians = rd.number();
auto operation = (SmOffsetOperationType)rd.index(4);
bool inset = rd.flag(), extend = rd.flag(), merge = rd.flag(), solid = rd.flag();
bool install = rd.flag();
int64_t norig = rd.index(), noffset = rd.index(), nfaces = rd.index();
SmContext ctx;
auto original = brep(ctx), offset = brep(ctx), face_owner = brep(ctx);
for (int64_t i=0; i<norig; ++i) unit_wire(ctx, original.get(), (double)i);
for (int64_t i=0; i<noffset; ++i) unit_wire(ctx, offset.get(), (double)i);
for (int64_t i=0; i<nfaces; ++i) plane_face(ctx, face_owner.get(), (double)i);
SmTArray<SmEdgeuse*> originals, offsets;
SmTArray<SmFace*> faces;
original->GetEdgeuses(originals); offset->GetEdgeuses(offsets); face_owner->GetFaces(faces);
ConfigGeometry geometry(distance, tol, tangency_tol_radians);
Executive executive(ctx, operation, geometry, original.get());
out.push_back(0);
state(out, executive, geometry);
executive.SetInset(inset ? TRUE : FALSE);
executive.SetExtendConvexEdges(extend ? TRUE : FALSE);
executive.SetMergeResults(merge ? TRUE : FALSE);
executive.SetCreateOffsetSolid(solid ? TRUE : FALSE);
if (install) executive.install(std::move(offset)); // only executive deletes this B-rep.
std::vector<double> shell_records;
int64_t nsets = rd.index();
shell_records.push_back((double)nsets);
for (int64_t i=0; i<nsets; ++i) {
SmTArray<SmFace*> selectors, result;
int64_t n = rd.index();
for (int64_t j=0; j<n; ++j) {
int64_t index = rd.index();
if (index >= faces.GetSize()) throw Argument{};
selectors.Add(faces[(ULONG)index]);
}
executive.SetShellFaces(selectors);
// Mutating the caller's array checks SetShellFaces' copy, and prefilling
// the destination checks that GetShellFaces resets, rather than appends.
selectors.ReSet();
if (faces.GetSize()) result.Add(faces[0]);
executive.GetShellFaces(result);
shell_records.push_back(result.GetSize());
for (ULONG j=0; j<result.GetSize(); ++j) shell_records.push_back(id(faces, result[j]));
}
state(out, executive, geometry);
out.insert(out.end(), shell_records.begin(), shell_records.end());
int64_t npairs = rd.index();
std::vector<double> statuses;
for (int64_t i=0; i<npairs; ++i) {
int64_t a = rd.index(max_items*2), b = rd.index(max_items*2);
if (a >= originals.GetSize() || b >= offsets.GetSize()) throw Argument{};
statuses.push_back(executive.relate(originals[(ULONG)a], offsets[(ULONG)b]));
}
rd.done();
out.push_back(originals.GetSize());
for (ULONG i=0; i<originals.GetSize(); ++i) {
SmTArray<SmEdgeuse*> seconds;
out.push_back(id(offsets, executive.GetOffsetEdgeuse(originals[i])));
executive.seconds(originals[i], seconds);
put_ids(out, seconds, offsets);
}
out.push_back((double)statuses.size());
out.insert(out.end(), statuses.begin(), statuses.end());
}
// Give each mapped wire a genuine two-edge loop. The second edge closes the
// loop and is fixture scaffolding only. No UV trims are requested: synchronization
// concerns the 3D edge ranges and must also accept nonplanar B-spline curves.
SmEdge* loop_fixture(SmContext& ctx, SmBrep* target, std::unique_ptr<SmCurve> curve,
const SmExtent1d& interval, std::vector<SmEdge*>& supports) {
SmEdge* edge = make_wire(ctx, target, std::move(curve), interval);
SmVertex* a = edge->GetStartVertex();
SmVertex* b = edge->GetOtherVertex(a);
// A B-spline closing segment also represents coincident endpoints without
// requiring analytic-line direction normalization (zero-width inputs stay valid).
SmBSplineCurve* closing = nullptr;
SmStatus status = SmBSplineCurve::CreateLineSegment(ctx, 3, b->GetPoint(), a->GetPoint(), closing);
std::unique_ptr<SmCurve> closing_owner(closing);
constructed(status);
if (!closing) throw std::bad_alloc();
SmExtent1d unit = closing->GetNaturalInterval();
struct PendingCurve {
SmCurve* p;
~PendingCurve() { if (p && !p->GetOwner()) delete p; }
} pending_curve{closing_owner.release()};
SmEdge* support = nullptr;
constructed(target->MakeWireEdge(nullptr, b, a, pending_curve.p,
unit, SM_OT_SAME, support));
if (!support) throw Argument{};
supports.push_back(support);
SmTArray<ULONG> loop_sizes;
SmTArray<SmEdgeuse*> uses;
SmTArray<SmVertex*> vertices;
loop_sizes.Add(2);
uses.Add(edge->GetPrimaryEdgeuse()); uses.Add(support->GetPrimaryEdgeuse());
SmExtent2d domain(SmPoint2d(0,0), SmPoint2d(1,1));
struct PendingSurface {
SmSurface* p;
~PendingSurface() { if (p && !p->GetOwner()) delete p; }
} plane{new (ctx) SmPlane(SmPoint3d(0,0,0), SmVector3d(1,0,0), SmVector3d(0,1,0),
SmVector2d(1,1), domain, &ctx)};
SmRegion* region = nullptr;
SmShell* shell = nullptr;
SmFace* face = nullptr;
constructed(target->MakeFaceTopology(nullptr, plane.p, SM_OT_SAME, loop_sizes, uses, vertices,
region, shell, face, FALSE, FALSE));
SmEdgeuse* use = edge->GetPrimaryEdgeuse();
if (!use || !use->GetCCWEdgeuse() || !use->GetCWEdgeuse()) throw Argument{};
return edge;
}
// Native traversal restricted to the measured edges: auxiliary closing edges are
// not part of the unit's inputs or output topology. Split descendants remain here.
void measured_traversal(SmBrep* target, const std::vector<SmEdge*>& supports,
SmTArray<SmEdge*>& edges, SmTArray<SmEdgeuse*>& uses) {
edges.ReSet(); uses.ReSet();
SmTArray<SmEdge*> all;
target->GetEdges(all);
for (ULONG i=0; i<all.GetSize(); ++i) {
if (std::find(supports.begin(), supports.end(), all[i]) != supports.end()) continue;
edges.Add(all[i]);
SmTArray<SmEdgeuse*> pair;
all[i]->GetEdgeuses(pair);
uses.Append(pair);
}
}
void put_eu_records(std::vector<double>& out, const SmTArray<SmEdge*>& edges,
const SmTArray<SmEdgeuse*>& uses) {
out.push_back(uses.GetSize());
for (ULONG i=0; i<uses.GetSize(); ++i) {
SmEdge* edge = uses[i]->GetEdge();
const SmExtent1d& interval = edge->GetInterval();
SmCurve* curve = edge->GetCurve();
out.insert(out.end(), {(double)i, id(edges, edge), interval.GetMin(), interval.GetMax(),
curve->ApproximateLength(interval, 10)});
SmPoint3d point(0,0,0);
out.push_back(curve->EvaluatePoint(interval.GetMin(), point));
put_point(out, point);
point.Set(0,0,0);
out.push_back(curve->EvaluatePoint(interval.GetMax(), point));
put_point(out, point);
}
}
void synchronize(Reader& rd, std::vector<double>& out) {
bool null_original = rd.flag(), null_other = rd.flag();
SmContext ctx;
auto original = brep(ctx), offset = brep(ctx);
SmEdgeuse* a = unit_wire(ctx, original.get(), 0)->GetPrimaryEdgeuse();
SmEdgeuse* b = unit_wire(ctx, original.get(), 1)->GetPrimaryEdgeuse();
SmTArray<SmEdgeuse*> first, second;
std::vector<SmEdge*> supports;
for (int side=0; side<2; ++side) {
int64_t n = rd.index();
for (int64_t i=0; i<n; ++i) {
auto curve = read_curve(rd, ctx);
SmExtent1d interval = rd.interval();
SmEdge* edge = loop_fixture(ctx, offset.get(), std::move(curve), interval, supports);
(side == 0 ? first : second).Add(edge->GetPrimaryEdgeuse());
}
}
rd.done();
SmOffsetGeometryCreation geometry(1, 1e-6);
Executive executive(ctx, SM_OO_SOLID_OFFSET, geometry, original.get());
SmBrep* target = offset.get();
executive.install(std::move(offset));
for (ULONG i=0; i<first.GetSize(); ++i) {
SmStatus status = executive.relate(a, first[i]);
if (status != SM_SUCCESS) throw Argument{};
}
if (!null_other) for (ULONG i=0; i<second.GetSize(); ++i) {
SmStatus status = executive.relate(b, second[i]);
if (status != SM_SUCCESS) throw Argument{};
}
if (null_original) a = nullptr;
if (null_other) b = nullptr;
SmTArray<SmEdgeuse*> before, related_a, related_b;
SmTArray<SmEdge*> measured_edges;
measured_traversal(target, supports, measured_edges, before);
double lookup_a = id(before, executive.GetOffsetEdgeuse(a));
double lookup_b = id(before, executive.GetOffsetEdgeuse(b));
executive.seconds(a, related_a); executive.seconds(b, related_b);
std::vector<double> relation_records;
put_ids(relation_records, related_a, before); put_ids(relation_records, related_b, before);
SmTArray<SmEdgeuse*> matched_a, matched_b, gap_a, gap_b;
SmStatus status = executive.SyncronizeOffsetEdgeuses(a, b, matched_a, matched_b, gap_a, gap_b);
out.insert(out.end(), {0, (double)status, lookup_a, lookup_b});
SmTArray<SmEdgeuse*> after, fixtures;
measured_traversal(target, supports, measured_edges, after);
fixtures.Append(first); fixtures.Append(second);
put_ids(out, fixtures, after);
out.insert(out.end(), relation_records.begin(), relation_records.end());
put_eu_records(out, measured_edges, after);
put_ids(out, matched_a, after); put_ids(out, matched_b, after);
put_ids(out, gap_a, after); put_ids(out, gap_b, after);
}
// NL_CrvAdv.cpp's G1 extension retries a zero end tangent using Pw[2] or
// Pw[n-2], even for a two-point curve. Check the native isocurves sampled by
// NL_SrfAdv.cpp first: otherwise an accepted degenerate bilinear fixture can
// read outside its control polygon. This is a safety check, not an extension
// implementation, and does not reject short but normalizable tangents.
void guard_two_point_isocurves(SmSurface* base) {
SmBSplineSurface* spline = SM_CAST_PTR(SmBSplineSurface, base);
if (!spline) return;
gw_SURFACE* surface = spline->GetOrCreateGwNurbPointer();
if (!surface) throw Argument{};
for (int direction=0; direction<2; ++direction) {
if ((direction == 0 ? surface->net->n : surface->net->m) != 1) continue;
gw_KNOTVECTOR* cross_knots = direction == 0 ? surface->knv : surface->knu;
for (double fraction : {0.07, 0.19, 0.45, 0.73, 0.92}) {
double parameter = (1.0-fraction)*cross_knots->U[0]
+ fraction*cross_knots->U[cross_knots->m];
std::unique_ptr<gw_CURVE, decltype(&sm_FreeNurbCurve)> curve(
sm_AllocateNurbCurve(1, 1, 3), &sm_FreeNurbCurve);
if (!curve) throw std::bad_alloc();
if (N_SrfExtractIsoCrv(surface, parameter, direction == 0 ? NL_UDIR : NL_VDIR,
curve.get(), nullptr) != 0) continue;
NL_POINT a, b;
NL_VECTOR tangent, unit;
NL_REAL magnitude = 0;
N_CPtToPtEuclid(curve->pol->Pw[0], &a);
N_CPtToPtEuclid(curve->pol->Pw[1], &b);
N_VectorDir(a, b, &tangent);
if (N_VectorNormalize(tangent, &unit, &magnitude) != 0) throw Argument{};
}
}
}
void extend(Reader& rd, std::vector<double>& out) {
double distance = rd.number(), tol = rd.number();
SmContext ctx;
std::vector<std::unique_ptr<SmSurface>> bases, extended;
int64_t n = rd.index();
for (int64_t i=0; i<n; ++i) bases.push_back(read_surface(rd, ctx));
std::vector<SmPoint2d> samples;
int64_t nsamples = rd.index();
for (int64_t i=0; i<nsamples; ++i) samples.push_back(rd.uv());
auto original = brep(ctx);
SmOffsetGeometryCreation geometry(distance, tol);
Executive executive(ctx, SM_OO_SOLID_OFFSET, geometry, original.get());
int64_t nactions = rd.index();
out.insert(out.end(), {0, (double)nactions});
for (int64_t i=0; i<nactions; ++i) {
int64_t action = rd.index(2);
if (action == 2) {
out.insert(out.end(), {2, (double)executive.cache_count(), 0});
executive.clear();
continue;
}
double raw_key = rd.number();
if (raw_key < -1 || std::floor(raw_key) != raw_key || raw_key >= (double)bases.size()
|| (action == 1 && raw_key < 0)) throw Argument{};
SmSurface* base = raw_key == -1 ? nullptr : bases.at((size_t)raw_key).get();
bool hit = executive.cached(base) != nullptr;
if (action == 1) {
executive.forget(base);
out.insert(out.end(), {1, hit ? 1.0 : 0.0, (double)executive.cache_count()});
continue;
}
if (base && !hit) guard_two_point_isocurves(base);
SmExtent2d domain;
SmSurface* result = nullptr;
SmStatus status = executive.GetCreateExtendedSurface(base, domain, result);
// SmOffsetExecutive deletes its map, NOT the extended surfaces. Keep each
// distinct returned allocation alive through eviction/clear and clean it
// after the executive has destroyed the map. Never wrap a base alias twice.
double result_id = -1;
if (result) {
for (size_t j=0; j<extended.size(); ++j)
if (extended[j].get() == result) result_id = (double)j;
if (result_id == -1) {
if (std::any_of(bases.begin(), bases.end(),
[result](const std::unique_ptr<SmSurface>& p){ return p.get() == result; }))
throw Argument{}; // unsupported ownership contract, not a fixture rejection.
result_id = (double)extended.size();
extended.emplace_back(result);
}
}
out.insert(out.end(), {0, (double)status, hit ? 1.0 : 0.0, result_id, result ? 1.0 : 0.0});
if (status != SM_SUCCESS) continue;
if (!result) throw Argument{};
out.push_back(result->IsKindOf(SmPlane_TYPE) ? 1 :
result->IsKindOf(SmBSplineSurface_TYPE) ? 2 : 3);
put_domain(out, domain);
out.push_back((double)samples.size());
for (const SmPoint2d& fraction : samples) {
SmPoint2d uv((1.0-fraction.x)*domain.GetMin().x + fraction.x*domain.GetMax().x,
(1.0-fraction.y)*domain.GetMin().y + fraction.y*domain.GetMax().y);
SmPoint3d point(0,0,0);
SmVector3d normal(0,0,0);
out.push_back(result->EvaluatePoint(uv, point)); put_point(out, point);
out.push_back(result->EvaluateNormal(uv, TRUE, TRUE, normal)); put_point(out, normal);
}
}
rd.done();
}
void probe(Reader& rd, std::vector<double>& out) {
int64_t operation = rd.index(12);
double tol = rd.number();
rd.done();
SmContext ctx;
auto original = brep(ctx), offset = brep(ctx);
SmOffsetGeometryCreation geometry(0.25, tol);
Executive executive(ctx, SM_OO_SOLID_OFFSET, geometry, original.get());
executive.install(std::move(offset));
SmTArray<SmCurve*> curves;
SmTArray<SmOrientType> orientations;
SmSurface* surface = nullptr;
SmBrep* result = nullptr;
SmStatus status = SM_SUCCESS;
SmTArray<SmEdgeuse*> originals, offsets;
switch (operation) {
case 0: status = SmOffsetExecutive::ReplaceImplicitOffsets(original.get(), tol); break;
case 1: status = executive.RemoveExcessFaces(executive.GetOffsetBrep()); break;
case 2:
executive.SetMergeResults(TRUE);
status = executive.RemoveExcessFaces(executive.GetOffsetBrep()); break;
case 3: status = executive.BuildSideWallOfEdgeuse(nullptr); break;
case 4: status = executive.BuildSideWallOfEdgeuse(unit_wire(ctx, original.get(), 0)->GetPrimaryEdgeuse()); break;
case 5: {
SmTArray<SmEdgeuse*> gaps;
SmTArray<SmFace*> faces;
status = executive.FillGapEdgeuses(nullptr, gaps, faces); break;
}
case 6: status = executive.BuildOffsetOfVertex(nullptr, curves, orientations, surface); break;
case 7: {
SmShell* shell = nullptr;
SmVertex* vertex = nullptr;
constructed(original->MakeShellVertex(nullptr, SmPoint3d(0,0,0), shell, vertex));
status = executive.BuildOffsetOfVertex(vertex, curves, orientations, surface); break;
}
case 8: case 9: {
SmSSIData intersections;
SmFaceuse* use = nullptr;
if (operation == 9) use = plane_face(ctx, original.get(), 0)->GetUpwardFaceuse();
status = executive.BuildOffsetOfFaceuse(use, surface, intersections); break;
}
case 10: status = executive.PiecewiseMerge(result); break;
case 11: {
SmFace* face = plane_face(ctx, original.get(), 0);
status = executive.MapEdgeuses(face->GetUpwardFaceuse(), face);
original->GetEdgeuses(originals); offsets.Append(originals);
break;
}
case 12: {
SmFace* face = plane_face(ctx, original.get(), 0);
face->GetUpwardFaceuse()->GetEdgeuses(originals);
if (!originals.GetSize()) throw Argument{};
SmEdgeuse* use = originals[0];
if (!use->GetRadial()) throw Argument{};
status = executive.BuildOffsetOfEdgeuses(use, use->GetRadial(), SM_MT_MARK);
originals.ReSet();
break;
}
}
// PiecewiseMerge transfers ownership out of m_pOffsetBrep before stitching.
std::unique_ptr<SmBrep> result_owner(result);
std::unique_ptr<SmSurface> surface_owner(surface);
out.insert(out.end(), {0, (double)status, (double)curves.GetSize(), (double)orientations.GetSize(),
surface ? 1.0 : 0.0, result ? 1.0 : 0.0,
executive.GetOffsetBrep() ? 1.0 : 0.0});
out.push_back(originals.GetSize());
for (ULONG i=0; i<originals.GetSize(); ++i)
out.push_back(id(offsets, executive.GetOffsetEdgeuse(originals[i])));
for (ULONG i=0; i<curves.GetSize(); ++i) delete curves[i];
}
} // namespace
// Plain-C ABI, mode 0 config / 1 synchronize / 2 extend / 3 probe.
// Caller owns every buffer; no native allocation or object address crosses it.
SC_EXPORT int32_t sc_offset_offset_executive_p1_eval(
int32_t mode, const double* input, int64_t input_len,
double* out, int64_t cap, int64_t* out_len) {
if (!out_len || cap < 0 || (cap && !out) || !input || input_len < 0 || input_len > max_values)
return SC_ERR_ARG;
try {
Reader rd{input, input_len};
std::vector<double> result;
try {
switch (mode) {
case 0: config(rd, result); break;
case 1: synchronize(rd, result); break;
case 2: extend(rd, result); break;
case 3: probe(rd, result); break;
default: throw Argument{};
}
} catch (const Rejected& rejection) {
result = {-1, (double)rejection.status};
}
return sc_copy_out(result.data(), (int64_t)result.size(), out, cap, out_len);
} catch (const Argument&) {
return SC_ERR_ARG;
} catch (...) {
return SC_ERR_EXCEPTION;
}
}