port: geom.context (SmContext, SmNewMarkAndLock, SM_API context)

Adds geom::context (Context with marks/locks, Boolean flag, notify
callbacks, own model size / zone tolerance; MarkLock; MassPropertyBehavior;
create/get_or_create_context), Kernel::context, and wires the
UNPORTED(geom.context) SmTol context overloads (Option<&Context>).
Difftest: 2013 cases, all bit-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
bherbruck
2026-10-12 15:59:25 -04:00
parent 8b439b87f7
commit d646586d30
11 changed files with 18739 additions and 39 deletions
+558
View File
@@ -0,0 +1,558 @@
//! Runner for unit `geom.context`: context marks and mark locks, the Boolean flag,
//! notify callbacks, the context model size estimate / zone tolerance and the
//! `SmTol` context overloads, `SmMassPropertyBehavior`, the `SM_API` context.
//!
//! C++: `solidcore_ffi::units::geom_context` (upstream `SmContext`,
//! `SmNewMarkAndLock`, `source/SMLib/{inc,src}/SmContext.*`).
//! Rust: `solidcore::geom::context`, `solidcore::geom::tol`.
//!
//! Every case is a script of [`Step`]s run on `contexts` (model size, ratio) with the
//! system tolerances set from `sys_model` / `sys_ratio` and `slots` mark-lock slots.
//! Output: `i` (integers: marks, locks, flags, mark types, context indices) and `f`
//! (floats: model sizes, ratios, tolerances), each step's results followed by a
//! snapshot of every context, and a final snapshot after the live locks are dropped
//! in slot order. Every run uses at least [`MIN_GENERATED`] generated scripts.
use serde::{Deserialize, Serialize};
use serde_json::Value;
use solidcore::geom::context::{
self as ctxmod, Context, MarkLock, MarkType, MassPropertyBehavior, NotifyCallback,
};
use solidcore::geom::tol::{self, SmallTopoType};
use solidcore::{Kernel, KernelOptions};
use solidcore_ffi::units::geom_context::{self as ffi, op, Item, Step};
use crate::case::CaseContext;
use crate::runner::default_cases;
use crate::{Case, HarnessError, Output, Rng, UnitDiff};
/// Generated scripts per run, whatever `--seeds` says (it can only raise this).
pub const MIN_GENERATED: usize = 2000;
/// `geom.context` runner.
pub struct Runner;
/// One script step as written in fixtures (absent fields are 0).
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(default)]
struct StepIn {
op: i64,
slot: i64,
ctx: i64,
ctx2: i64,
m: i64,
flag: i64,
x: [f64; 4],
}
impl From<StepIn> for Step {
fn from(s: StepIn) -> Self {
Self {
op: s.op,
slot: s.slot,
ctx: s.ctx,
ctx2: s.ctx2,
m: s.m,
flag: s.flag,
x: s.x,
}
}
}
fn default_size() -> f64 {
tol::USE_DEFAULT
}
fn default_contexts() -> Vec<[f64; 2]> {
vec![[tol::USE_DEFAULT, tol::USE_DEFAULT]]
}
fn default_slots() -> usize {
4
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Inputs {
#[serde(default = "default_size")]
sys_model: f64,
#[serde(default = "default_size")]
sys_ratio: f64,
#[serde(default = "default_contexts")]
contexts: Vec<[f64; 2]>,
#[serde(default = "default_slots")]
slots: usize,
#[serde(default)]
steps: Vec<StepIn>,
}
impl Inputs {
fn steps(&self) -> Vec<Step> {
self.steps.iter().map(|&s| Step::from(s)).collect()
}
}
fn output(items: &[Item]) -> Output {
let mut ints = Vec::new();
let mut floats = Vec::new();
for it in items {
match *it {
Item::Int(v) => ints.push(v),
Item::Float(v) => floats.push(v),
}
}
Output::new().ints("i", ints).values("f", floats)
}
fn harness(e: impl std::fmt::Display) -> HarnessError {
HarnessError::new(e.to_string())
}
// ---- C++ side ----------------------------------------------------------------------
fn cpp(i: &Inputs) -> Result<Output, HarnessError> {
let contexts: Vec<(f64, f64)> = i.contexts.iter().map(|c| (c[0], c[1])).collect();
let items =
ffi::script(i.sys_model, i.sys_ratio, &contexts, i.slots, &i.steps()).map_err(harness)?;
Ok(output(&items))
}
// ---- Rust side ---------------------------------------------------------------------
/// The kernel `SmTol::SetModelSizeEstimate(model)` +
/// `SmTol::SetLargeSmallSizeRatio(ratio)` would leave behind.
fn kernel(model: f64, ratio: f64) -> Kernel {
let model = if model == tol::USE_DEFAULT {
tol::MODEL_SIZE_ESTIMATE
} else {
model
};
let ratio = if ratio == tol::USE_DEFAULT {
tol::LARGE_SMALL_SIZE_RATIO
} else {
ratio
};
let zone = tol::size_estimate_to_zone_tol_3d(model);
let mut o = KernelOptions::default();
o.tolerances.model_size_estimate = model;
o.tolerances.zone_tol_3d = zone;
o.tolerances.xsect_tol_3d = zone * 2.0;
o.tolerances.approx_tol_3d = zone / 2.0;
o.tolerances.large_small_size_ratio = ratio;
Kernel::new(&o)
}
/// The callback object installed by `CALLBACK` steps.
#[derive(Debug)]
struct Notify;
impl NotifyCallback for Notify {}
const MARKS: [MarkType; 7] = [
MarkType::NoMark,
MarkType::Mark,
MarkType::Mark2,
MarkType::Mark3,
MarkType::MarkIo,
MarkType::MarkAssert,
MarkType::AllMarks,
];
struct Sim {
k: Kernel,
ctxs: Vec<Context>,
slots: Vec<Option<MarkLock>>,
out: Vec<Item>,
api: Option<Context>,
}
fn idx(v: i64) -> Result<usize, HarnessError> {
usize::try_from(v).map_err(|_| harness(format!("index {v}")))
}
fn mark(v: i64) -> Result<MarkType, HarnessError> {
i32::try_from(v)
.ok()
.and_then(MarkType::from_code)
.ok_or_else(|| harness(format!("mark type {v}")))
}
impl Sim {
fn int(&mut self, v: i64) {
self.out.push(Item::Int(v));
}
fn float(&mut self, v: f64) {
self.out.push(Item::Float(v));
}
fn ctx(&mut self, i: i64) -> Result<&mut Context, HarnessError> {
let i = idx(i)?;
self.ctxs.get_mut(i).ok_or_else(|| harness("context index"))
}
fn opt_ctx(&self, i: i64) -> Option<&Context> {
usize::try_from(i).ok().and_then(|i| self.ctxs.get(i))
}
fn slot(&mut self, s: i64) -> Result<&mut Option<MarkLock>, HarnessError> {
let s = idx(s)?;
self.slots.get_mut(s).ok_or_else(|| harness("slot index"))
}
/// `delete slot`: runs the lock's destructor.
fn drop_slot(&mut self, s: i64) -> Result<(), HarnessError> {
if let Some(mut l) = self.slot(s)?.take() {
l.release(&mut self.ctxs).map_err(harness)?;
}
Ok(())
}
fn snapshot(&mut self) {
let mut v = Vec::new();
for c in &self.ctxs {
for m in MARKS {
v.push(Item::Int(c.current_mark(m) as i64));
v.push(Item::Int(i64::from(c.is_mark_locked(m))));
}
v.push(Item::Int(i64::from(c.doing_boolean())));
v.push(Item::Int(i64::from(c.sys_notify_callback().is_some())));
v.push(Item::Int(i64::from(c.user_notify_callback().is_some())));
v.push(Item::Float(c.stored_model_size_estimate()));
v.push(Item::Float(c.stored_large_small_size_ratio()));
v.push(Item::Float(c.this_zone_tol_3d()));
}
self.out.extend(v);
}
fn step(&mut self, s: &Step) -> Result<(), HarnessError> {
let x = s.x;
match s.op {
op::CUR_MARK => {
let m = mark(s.m)?;
let v = self.ctx(s.ctx)?.current_mark(m);
self.int(v as i64);
}
op::IS_LOCKED => {
let m = mark(s.m)?;
let v = self.ctx(s.ctx)?.is_mark_locked(m);
self.int(i64::from(v));
}
op::LOCK_NEW => {
self.drop_slot(s.slot)?;
let l = MarkLock::new(&mut self.ctxs, idx(s.ctx)?, s.m as i32).map_err(harness)?;
*self.slot(s.slot)? = Some(l);
self.int(i64::from(l.mark_type().code()));
}
op::LOCK_PAIR => {
self.drop_slot(s.slot)?;
let l = MarkLock::new_pair(&mut self.ctxs, idx(s.ctx)?, idx(s.ctx2)?)
.map_err(harness)?;
*self.slot(s.slot)? = Some(l);
self.int(i64::from(l.mark_type1().code()));
self.int(i64::from(l.mark_type2().code()));
}
op::LOCK_DEFAULT => {
self.drop_slot(s.slot)?;
*self.slot(s.slot)? = Some(MarkLock::default());
}
op::LOCK_NEWMARK => match *self.slot(s.slot)? {
Some(l) if l.context().is_some() => {
l.new_mark(&mut self.ctxs).map_err(harness)?;
self.int(1);
}
_ => self.int(-1),
},
op::LOCK_SETCTX | op::LOCK_SETCTX_PAIR => {
let mut l = self.slot(s.slot)?.unwrap_or_else(MarkLock::unbound);
let m = if s.op == op::LOCK_SETCTX {
l.set_context(&mut self.ctxs, idx(s.ctx)?, s.m as i32)
} else {
l.set_context_pair(&mut self.ctxs, idx(s.ctx)?, idx(s.ctx2)?)
}
.map_err(harness)?;
*self.slot(s.slot)? = Some(l);
self.int(i64::from(m.code()));
}
op::LOCK_DROP => self.drop_slot(s.slot)?,
op::LOCK_INFO => match *self.slot(s.slot)? {
None => self.int(-2),
Some(l) => {
let ix = |c: Option<usize>| c.map_or(-1, |c| c as i64);
self.int(i64::from(l.mark_type().code()));
self.int(i64::from(l.mark_type1().code()));
self.int(i64::from(l.mark_type2().code()));
self.int(ix(l.context()));
self.int(ix(l.context2()));
}
},
op::DOING_BOOL => {
let c = self.ctx(s.ctx)?;
if s.flag != 0 {
*c.doing_boolean_mut() = s.m != 0;
} else {
c.set_doing_boolean(s.m != 0);
}
let v = c.doing_boolean();
self.int(i64::from(v));
}
op::GET_MODEL => {
let c = self.opt_ctx(s.ctx).ok_or_else(|| harness("context index"))?;
let a = c.this_model_size_estimate(&self.k);
let b = tol::context_model_size_estimate(&self.k, c);
self.float(a);
self.float(b);
}
op::SET_MODEL => {
let c = self.ctx(s.ctx)?;
let z = if s.flag != 0 {
tol::set_context_model_size_estimate(c, x[0])
} else {
c.set_this_model_size_estimate(x[0])
};
self.float(z);
}
op::SET_RATIO => self.ctx(s.ctx)?.set_this_large_small_size_ratio(x[0]),
op::TOL_CTX => {
let small = i32::try_from(s.m)
.ok()
.and_then(SmallTopoType::from_code)
.ok_or_else(|| harness("small topology type"))?;
let c = self.opt_ctx(s.ctx);
let k = &self.k;
let v = [
tol::context_zone_tol_3d(k, c, small),
tol::context_approx_tol_3d(k, c, small),
tol::context_xsect_tol_3d(k, c, small),
tol::calc_face_zone_tol_3d(k, c),
];
for f in v {
self.float(f);
}
}
op::CALC_EDGE | op::CALC_VERTEX => {
let c = self.opt_ctx(s.ctx);
let k = &self.k;
let max = (s.flag != 0).then(|| tol::zone_tol3d(k, x[2]));
let v = if s.op == op::CALC_EDGE {
tol::calc_edge_zone_tol_3d(k, x[0], x[1], max, c)
} else {
tol::calc_vertex_zone_tol_3d(k, x[0], x[1], max, c)
};
self.float(v);
}
op::CALLBACK => {
let c = self.ctx(s.ctx)?;
let cb: Option<Box<dyn NotifyCallback>> = if s.m != 0 {
Some(Box::new(Notify))
} else {
None
};
if s.flag != 0 {
c.set_user_notify_callback(cb);
} else {
c.set_sys_notify_callback(cb);
}
let a = c.sys_notify_callback().is_some();
let b = c.user_notify_callback().is_some();
self.int(i64::from(a));
self.int(i64::from(b));
}
op::MASS => {
let r = if s.slot != 0 {
MassPropertyBehavior::default()
} else {
MassPropertyBehavior::new(x[0], x[1], x[2], x[3], s.m as u64, s.flag as u64)
};
self.float(r.density);
self.float(r.face_thickness);
self.float(r.edge_cross_section_radius);
self.float(r.vertex_sphere_radius);
self.int(r.face_area_behavior as i64);
self.int(r.face_volume_behavior as i64);
}
op::API => {
ctxmod::create_context(&mut self.api);
let p: *const Context = ctxmod::get_or_create_context(&mut self.api);
let q: *const Context = ctxmod::get_or_create_context(&mut self.api);
self.int(1);
self.int(i64::from(std::ptr::eq(p, q)));
}
other => return Err(harness(format!("op {other}"))),
}
Ok(())
}
}
fn rust(i: &Inputs) -> Result<Output, HarnessError> {
let steps = i.steps();
// reject what the C++ wrapper rejects, so both sides fail alike
ffi::validate(i.contexts.len(), i.slots, &steps).map_err(harness)?;
let mut sim = Sim {
k: kernel(i.sys_model, i.sys_ratio),
ctxs: i.contexts.iter().map(|c| Context::new(c[0], c[1])).collect(),
slots: vec![None; i.slots],
out: Vec::new(),
api: None,
};
for s in &steps {
sim.step(s)?;
sim.snapshot();
}
for s in 0..i.slots {
sim.drop_slot(s as i64)?;
}
sim.snapshot();
Ok(output(&sim.out))
}
// ---- generation --------------------------------------------------------------------
/// A model size estimate or ratio: the default sentinel, table boundaries, or
/// log-uniform values over many decades (sometimes negative).
fn size(rng: &mut Rng) -> f64 {
const EDGES: [f64; 9] = [0.00005, 0.005, 0.5, 500.0, 50000.0, 5e6, 50.0, 1000.0, 1.0];
let r = rng.unit();
if r < 0.35 {
tol::USE_DEFAULT
} else if r < 0.6 {
rng.choose(&EDGES).copied().unwrap_or(50.0)
} else {
let v = 10f64.powf(rng.uniform(-6.0, 8.0));
if rng.chance(0.1) {
-v
} else {
v
}
}
}
fn gen_step(rng: &mut Rng, n_ctx: i64, n_slots: i64) -> StepIn {
const WEIGHTS: [(i64, u64); 20] = [
(op::CUR_MARK, 3),
(op::IS_LOCKED, 2),
(op::LOCK_NEW, 8),
(op::LOCK_PAIR, 4),
(op::LOCK_DEFAULT, 1),
(op::LOCK_NEWMARK, 4),
(op::LOCK_SETCTX, 3),
(op::LOCK_SETCTX_PAIR, 2),
(op::LOCK_DROP, 6),
(op::LOCK_INFO, 2),
(op::DOING_BOOL, 2),
(op::GET_MODEL, 2),
(op::SET_MODEL, 2),
(op::SET_RATIO, 1),
(op::TOL_CTX, 2),
(op::CALC_EDGE, 1),
(op::CALC_VERTEX, 1),
(op::CALLBACK, 1),
(op::MASS, 1),
(op::API, 1),
];
let total: u64 = WEIGHTS.iter().map(|w| w.1).sum();
let mut r = rng.below(total);
let mut code = op::CUR_MARK;
for (c, w) in WEIGHTS {
if r < w {
code = c;
break;
}
r -= w;
}
let mut s = StepIn {
op: code,
slot: rng.range_i64(0, n_slots - 1),
ctx: rng.range_i64(0, n_ctx - 1),
ctx2: rng.range_i64(0, n_ctx - 1),
..StepIn::default()
};
let mark_type = |rng: &mut Rng| rng.choose(&ffi::MARK_TYPES).copied().unwrap_or(0);
match code {
op::CUR_MARK | op::IS_LOCKED | op::LOCK_SETCTX => s.m = mark_type(rng),
op::LOCK_NEW => {
s.m = if rng.chance(0.6) {
ctxmod::ALL_MARKS.into()
} else if rng.chance(0.1) {
rng.range_i64(-40, 100)
} else {
rng.range_i64(0, 31)
}
}
op::DOING_BOOL | op::CALLBACK => {
s.m = rng.range_i64(0, 1);
s.flag = rng.range_i64(0, 1);
}
op::SET_MODEL => {
s.flag = rng.range_i64(0, 1);
s.x[0] = size(rng);
}
op::SET_RATIO => s.x[0] = size(rng),
op::TOL_CTX | op::CALC_EDGE | op::CALC_VERTEX => {
if rng.chance(0.25) {
s.ctx = -1;
}
s.m = rng.range_i64(0, 4);
s.flag = rng.range_i64(0, 1);
s.x = [
rng.uniform(0.0, 1e-3),
rng.uniform(0.0, 1e-3),
rng.uniform(-1e-4, 1e-3),
0.0,
];
}
op::MASS => {
s.slot = i64::from(rng.chance(0.2));
s.m = rng.range_i64(0, 2);
s.flag = rng.range_i64(0, 1);
s.x = [
rng.uniform(0.0, 10.0),
rng.uniform(0.0, 1.0),
rng.uniform(0.0, 1.0),
rng.uniform(0.0, 1.0),
];
}
_ => {}
}
s
}
impl UnitDiff for Runner {
fn id(&self) -> &'static str {
"geom.context"
}
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()?)
}
fn generate(&self, rng: &mut Rng) -> Option<Value> {
let n_ctx = rng.range_i64(1, 3);
let n_slots = rng.range_i64(1, 6);
let n_steps = rng.range_i64(1, 40);
let contexts = (0..n_ctx).map(|_| [size(rng), size(rng)]).collect();
let steps = (0..n_steps)
.map(|_| gen_step(rng, n_ctx, n_slots))
.collect();
serde_json::to_value(Inputs {
sys_model: size(rng),
sys_ratio: size(rng),
contexts,
slots: n_slots as usize,
steps,
})
.ok()
}
}
+6 -4
View File
@@ -269,18 +269,20 @@ fn rust_values(op: Op, i: &Inputs) -> Result<solidcore::Result<Vec<f64>>, Harnes
Ok(match op {
Op::SizeEstimate => v1(tol::size_estimate_to_zone_tol_3d(a(0))),
Op::InTol => v1(flag(tol::in_tol(a(0), tol::tol3d(a(1))))),
Op::CalcFaceZone => v1(tol::calc_face_zone_tol_3d(&k)),
Op::CalcFaceZone => v1(tol::calc_face_zone_tol_3d(&k, None)),
Op::CalcEdgeZone => v1(tol::calc_edge_zone_tol_3d(
&k,
a(0),
a(1),
i.flag.then(|| tol::zone_tol3d(&k, a(2))),
None,
)),
Op::CalcVertexZone => v1(tol::calc_vertex_zone_tol_3d(
&k,
a(0),
a(1),
i.flag.then(|| tol::zone_tol3d(&k, a(2))),
None,
)),
Op::EffZeros => Ok(vec![
tol::eff_zero(),
@@ -313,11 +315,11 @@ fn rust_values(op: Op, i: &Inputs) -> Result<solidcore::Result<Vec<f64>>, Harnes
))),
Op::GapGain => v1(tol::gap_gain(&k)),
Op::ZoneSys => v1(tol::zone_tol_3d(&k, small)),
Op::ZoneCtx => v1(tol::context_zone_tol_3d(&k, small)),
Op::ZoneCtx => v1(tol::context_zone_tol_3d(&k, None, small)),
Op::ApproxSys => v1(tol::approx_tol_3d(&k, small)),
Op::ApproxCtx => v1(tol::context_approx_tol_3d(&k, small)),
Op::ApproxCtx => v1(tol::context_approx_tol_3d(&k, None, small)),
Op::XSectSys => v1(tol::xsect_tol_3d(&k, small)),
Op::XSectCtx => v1(tol::context_xsect_tol_3d(&k, small)),
Op::XSectCtx => v1(tol::context_xsect_tol_3d(&k, None, small)),
Op::ScaledZeroSys => Ok(vec![
tol::scaled_zero(),
tol::scaled_zero_sq(),
@@ -0,0 +1,208 @@
//! FFI wrappers for unit `geom.context`.
//!
//! Shim: `harness/cpp/shim/geom_context.cpp`.
//! Upstream: `source/SMLib/inc/SmContext.h`, `source/SMLib/src/SmContext.cpp`
//! (`SmContext`, `SmNewMarkAndLock`), the `SmContext` overloads of `SmTol`,
//! `SmMassPropertyBehavior` (`SmTopoTypes.h`) and `SmApiCreateContext` /
//! `SmApiGetOrCreateContext` (`SmApiGeneral.cpp`).
//!
//! | Rust | shim | upstream |
//! |------------|--------------------------|--------------------------------------------|
//! | [`script`] | `sc_geom_context_script` | a script of `SmContext` / `SmNewMarkAndLock` calls |
use crate::{call_with_buffer, FfiError};
mod sys {
extern "C" {
pub fn sc_geom_context_script(
sys_model: f64,
sys_ratio: f64,
ctx_params: *const f64,
n_ctx: i64,
ops: *const i64,
xs: *const f64,
n_ops: i64,
n_slots: i64,
out: *mut i64,
cap: i64,
out_len: *mut i64,
) -> i32;
}
}
/// Script operation codes (field use in brackets; see the shim for details).
pub mod op {
/// `GetCurrentMark(m)` [ctx, m].
pub const CUR_MARK: i64 = 0;
/// `IsMarkLocked(m)` [ctx, m].
pub const IS_LOCKED: i64 = 1;
/// `slot = new SmNewMarkAndLock(ctx, mask m)` [slot, ctx, m].
pub const LOCK_NEW: i64 = 2;
/// `slot = new SmNewMarkAndLock(ctx, ctx2)` [slot, ctx, ctx2].
pub const LOCK_PAIR: i64 = 3;
/// `slot = new SmNewMarkAndLock()` [slot].
pub const LOCK_DEFAULT: i64 = 4;
/// `slot->NewMark()` when bound [slot].
pub const LOCK_NEWMARK: i64 = 5;
/// `slot->SetContext(ctx, m)` [slot, ctx, m].
pub const LOCK_SETCTX: i64 = 6;
/// `slot->SetContext(ctx, ctx2)` [slot, ctx, ctx2].
pub const LOCK_SETCTX_PAIR: i64 = 7;
/// `delete slot` [slot].
pub const LOCK_DROP: i64 = 8;
/// `GetMarkType*`, `GetContext*` [slot].
pub const LOCK_INFO: i64 = 9;
/// `SetDoingBoolean(m)` or `GetDoingBooleanRef() = m` [ctx, m, flag].
pub const DOING_BOOL: i64 = 10;
/// `GetThisModelSizeEstimate()`, `SmTol::GetModelSizeEstimate(ctx)` [ctx].
pub const GET_MODEL: i64 = 11;
/// `SetThisModelSizeEstimate(x0)` or `SmTol::SetModelSizeEstimate(ctx, x0)` [ctx, flag].
pub const SET_MODEL: i64 = 12;
/// `SetThisLargeSmallSizeRatio(x0)` [ctx].
pub const SET_RATIO: i64 = 13;
/// `SmTol::GetZoneTol3d/GetApproxTol3d/GetXSectTol3d(ctx, m)`, `CalcFaceZoneTol3d(ctx)` [ctx|-1, m].
pub const TOL_CTX: i64 = 14;
/// `SmTol::CalcEdgeZoneTol3d(x0, x1, flag ? &x2 : NULL, ctx)` [ctx|-1, flag].
pub const CALC_EDGE: i64 = 15;
/// `SmTol::CalcVertexZoneTol3d(x0, x1, flag ? &x2 : NULL, ctx)` [ctx|-1, flag].
pub const CALC_VERTEX: i64 = 16;
/// `SetSysNotifyCallback` (flag 0) / `SetUserNotifyCallback` (flag 1), non-null if m [ctx, m, flag].
pub const CALLBACK: i64 = 17;
/// `SmMassPropertyBehavior(x0..x3, m, flag)`, or the default one if slot != 0.
pub const MASS: i64 = 18;
/// `SmApiCreateContext(); SmApiGetOrCreateContext()`.
pub const API: i64 = 19;
/// One past the last code.
pub const COUNT: i64 = 20;
}
/// The `SmMarkType` values.
pub const MARK_TYPES: [i64; 7] = [0, 1, 2, 4, 8, 16, 31];
/// Maximum number of contexts in a script.
pub const MAX_CONTEXTS: usize = 8;
/// Maximum number of lock slots in a script.
pub const MAX_SLOTS: usize = 16;
/// One script operation.
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Step {
/// Operation code ([`op`]).
pub op: i64,
/// Lock slot (or the "default" switch of [`op::MASS`]).
pub slot: i64,
/// Context index (`-1` = null where allowed).
pub ctx: i64,
/// Second context index.
pub ctx2: i64,
/// Mark type / mask / small-topology type / boolean / count operand.
pub m: i64,
/// Variant switch.
pub flag: i64,
/// Float operands.
pub x: [f64; 4],
}
/// One output item.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Item {
/// An integer result.
Int(i64),
/// A floating-point result.
Float(f64),
}
/// Checks a script the way the shim does (anything it would reject or crash on).
pub fn validate(n_ctx: usize, n_slots: usize, steps: &[Step]) -> Result<(), FfiError> {
let bad = |m: &str| Err(FfiError::InvalidArgument(m.into()));
if !(1..=MAX_CONTEXTS).contains(&n_ctx) {
return bad("context count");
}
if n_slots > MAX_SLOTS {
return bad("slot count");
}
let n_ctx = n_ctx as i64;
let n_slots = n_slots as i64;
for s in steps {
let o = s.op;
if !(0..op::COUNT).contains(&o) {
return bad("op code");
}
let need_slot = (op::LOCK_NEW..=op::LOCK_INFO).contains(&o);
let null_ok = matches!(o, op::TOL_CTX | op::CALC_EDGE | op::CALC_VERTEX);
let no_ctx = matches!(
o,
op::LOCK_DEFAULT | op::LOCK_NEWMARK | op::LOCK_DROP | op::LOCK_INFO | op::MASS | op::API
);
if need_slot && !(0..n_slots).contains(&s.slot) {
return bad("slot index");
}
let ctx_ok = (0..n_ctx).contains(&s.ctx) || (null_ok && s.ctx == -1);
if !no_ctx && !ctx_ok {
return bad("context index");
}
if matches!(o, op::LOCK_PAIR | op::LOCK_SETCTX_PAIR) && !(0..n_ctx).contains(&s.ctx2) {
return bad("second context index");
}
if matches!(o, op::CUR_MARK | op::IS_LOCKED | op::LOCK_SETCTX) && !MARK_TYPES.contains(&s.m)
{
return bad("mark type");
}
if o == op::DOING_BOOL && !(0..=1).contains(&s.m) {
return bad("boolean");
}
if o == op::TOL_CTX && !(0..=4).contains(&s.m) {
return bad("small topology type");
}
if o == op::MASS && (s.m < 0 || s.flag < 0) {
return bad("behavior");
}
}
Ok(())
}
/// Runs `steps` in upstream C++ on `contexts` (model size, ratio) with the system
/// tolerances set from (`sys_model`, `sys_ratio`) and reset afterwards. Returns per
/// step its results and a snapshot of every context, then a final snapshot after the
/// live locks are destroyed in slot order.
pub fn script(
sys_model: f64,
sys_ratio: f64,
contexts: &[(f64, f64)],
n_slots: usize,
steps: &[Step],
) -> Result<Vec<Item>, FfiError> {
validate(contexts.len(), n_slots, steps)?;
let params: Vec<f64> = contexts.iter().flat_map(|&(a, b)| [a, b]).collect();
let ops: Vec<i64> = steps
.iter()
.flat_map(|s| [s.op, s.slot, s.ctx, s.ctx2, s.m, s.flag])
.collect();
let xs: Vec<f64> = steps.iter().flat_map(|s| s.x).collect();
let raw = call_with_buffer(1024, |buf: &mut [i64], len| {
// SAFETY: params/ops/xs hold 2*n_ctx, 6*n_ops and 4*n_ops values; the script
// was validated above; buf is valid for buf.len() writes and len for one.
unsafe {
sys::sc_geom_context_script(
sys_model,
sys_ratio,
params.as_ptr(),
contexts.len() as i64,
ops.as_ptr(),
xs.as_ptr(),
steps.len() as i64,
n_slots as i64,
buf.as_mut_ptr(),
buf.len() as i64,
&raw mut *len,
)
}
})?;
raw.chunks(2)
.map(|c| match c {
[0, v] => Ok(Item::Int(*v)),
[1, v] => Ok(Item::Float(f64::from_bits(*v as u64))),
_ => Err(FfiError::InvalidArgument("malformed shim output".into())),
})
.collect()
}
+1
View File
@@ -6,6 +6,7 @@
//! `SmPseudoBox`, `SmTol`, `SmTransform`, `SmMatrix` and related `geom.*` units of
//! `plan/modules.toml`.
pub mod context;
pub mod extent3d;
pub mod tol;
pub mod vector2d;
+814
View File
@@ -0,0 +1,814 @@
//! Per-session modeling context (`SmContext`): traversal marks and their locks, the
//! "doing a Boolean" flag, notify callbacks and the context's own model size estimate
//! and zone tolerance; plus the mark-locking helper `SmNewMarkAndLock`, the
//! `SmMassPropertyBehavior` options and the `SM_API` context accessors.
//!
//! Upstream: `source/SMLib/inc/SmContext.h`, `source/SMLib/src/SmContext.cpp`,
//! `source/SMLib/inc/SmTopoTypes.h` (`SmMassPropertyBehavior`),
//! `source/SM_API/src/SmApiGeneral.cpp` (`SmApiCreateContext`, `SmApiGetOrCreateContext`).
//!
//! | Rust | Upstream |
//! |-------------------------------------------|-------------------------------------------------|
//! | [`Context`] | `SmContext` |
//! | [`Context::new`], [`Context::default`] | `SmContext::SmContext(double, double)` (no `SM_USE_GLOBAL_CACHE`) |
//! | [`Context::doing_boolean`] | `SmContext::GetDoingBoolean` |
//! | [`Context::doing_boolean_mut`] | `SmContext::GetDoingBooleanRef` |
//! | [`Context::set_doing_boolean`] | `SmContext::SetDoingBoolean` |
//! | [`Context::user_notify_callback`] | `SmContext::GetUserNotifyCallback` |
//! | [`Context::set_user_notify_callback`] | `SmContext::SetUserNotifyCallback` (unused upstream) |
//! | [`Context::sys_notify_callback`] | `SmContext::GetSysNotifyCallback` |
//! | [`Context::set_sys_notify_callback`] | `SmContext::SetSysNotifyCallback` |
//! | [`Context::this_model_size_estimate`] | `SmContext::GetThisModelSizeEstimate` |
//! | [`Context::set_this_model_size_estimate`] | `SmContext::SetThisModelSizeEstimate` (inline, `SM_USE_OLDTOL` build) |
//! | [`Context::set_this_large_small_size_ratio`] | `SmContext::SetThisLargeSmallSizeRatio` |
//! | [`Context::this_zone_tol_3d`] | `SmContext::m_sThisZoneTol3d` (`GetThisZoneTol3d`, unused upstream) |
//! | [`Context::stored_model_size_estimate`], [`Context::stored_large_small_size_ratio`] | the raw members `m_dThisModelSizeEstimate`, `m_dThisLargeSmallSizeRatio` |
//! | [`Context::current_mark`] | `SmContext::GetCurrentMark` |
//! | [`Context::is_mark_locked`] | `SmContext::IsMarkLocked` |
//! | `Context::new_mark` (private) | `SmContext::NewMark` (private, friend `SmNewMarkAndLock`) |
//! | [`MarkType`] | `SmMarkType` |
//! | [`MarkLock`] | `SmNewMarkAndLock` |
//! | [`MarkLock::new`] | `SmNewMarkAndLock(const SmContext*, SmBitArray)` |
//! | [`MarkLock::new_pair`] | `SmNewMarkAndLock(SmContext*, SmContext*)` |
//! | [`MarkLock::unbound`], [`MarkLock::default`] | `SmNewMarkAndLock()` |
//! | [`MarkLock::release`] | `SmNewMarkAndLock::~SmNewMarkAndLock` |
//! | [`MarkLock::mark_type`], [`MarkLock::mark_type1`], [`MarkLock::mark_type2`] | `GetMarkType`, `GetMarkType1`, `GetMarkType2` |
//! | [`MarkLock::context`] | `SmNewMarkAndLock::GetContext` |
//! | [`MarkLock::new_mark`] | `SmNewMarkAndLock::NewMark` |
//! | [`MarkLock::set_context`] | `SmNewMarkAndLock::SetContext(SmContext*, SmMarkType)` |
//! | [`MarkLock::set_context_pair`] | `SmNewMarkAndLock::SetContext(SmContext*, SmContext*)` |
//! | `lock_mark`, `unlock_mark` (private) | `SmNewMarkAndLock::LockMark`, `UnlockMark` |
//! | `unlocked_mark` (private) | `SmNewMarkAndLock::GetUnlockedMark` |
//! | [`with_mark_lock`] | the scoped `{ SmNewMarkAndLock sMarkLock(pContext, mask); ... }` idiom |
//! | [`MassPropertyBehavior`] | `SmMassPropertyBehavior` (and its constructor) |
//! | [`create_context`] | `SmApiCreateContext` |
//! | [`get_or_create_context`] | `SmApiGetOrCreateContext` |
//!
//! Deviations:
//! - `SmNewMarkAndLock` holds `SmContext*` pointers and unlocks in its destructor.
//! [`MarkLock`] instead stores the *indices* of its contexts in a caller-owned slice
//! `&mut [Context]` (index equality plays pointer equality) and every method that
//! touches a context takes that slice; the destructor is the explicit
//! [`MarkLock::release`]. [`with_mark_lock`] gives the scoped form for a
//! [`Kernel`]'s own context. A method that upstream would run through a null
//! context pointer (a crash) returns [`SolidError::InvalidInput`], as does an index
//! outside the slice.
//! - The upstream two-context constructor locks the *first* context's mark type in the
//! second context and increments the first context's mark twice; this is ported
//! as is (see `plan/findings.md`).
//! - `SM_ASSERT*`, `SM_ASSERT_MSG_BREAK` and `SM_DBG_WARN` only print messages in the
//! release build and are not ported. `SmBoolean` flags are `bool`; `ULONG` marks are
//! `u64` incremented with wrapping arithmetic, as in C.
//! - Notify callbacks are boxed [`NotifyCallback`] trait objects owned by the context
//! (upstream stores borrowed raw pointers). `Execute` is added to the trait by the
//! unit that ports `SmObject::Notify`.
//! - `SmApiGeneral.cpp` keeps one process-global `SmContext*`; the port takes the slot
//! (`&mut Option<Context>`) from the caller.
//! - The attribute-callback list, global user data (`void*`), `operator new/delete`,
//! `Dump`, `GetTypeString` and `IsLockedMark` are unused upstream and not ported.
use crate::error::{Result, SolidError};
use crate::geom::tol;
use crate::kernel::Kernel;
/// Which traversal mark of a context to use (`SmMarkType`). The discriminants are the
/// upstream bit values.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(i32)]
pub enum MarkType {
/// `SM_MT_NOMARK`: no mark (also the unset value of a [`MarkLock`]).
NoMark = 0,
/// `SM_MT_MARK`: the general mark.
Mark = 1,
/// `SM_MT_MARK2`: the second general mark.
Mark2 = 2,
/// `SM_MT_MARK3`: the third general mark.
Mark3 = 4,
/// `SM_MT_MARKIO`: the mark used by draw / IO methods.
MarkIo = 8,
/// `SM_MT_MARKASSERT`: the mark used by validity checks.
MarkAssert = 16,
/// `SM_MT_ALLMARKS`: all marks; as a single mark it behaves like an unknown
/// value (the `default:` branches upstream).
AllMarks = 31,
}
impl MarkType {
/// The upstream enum value (also the bit of the mark in a mark mask).
#[must_use]
pub const fn code(self) -> i32 {
self as i32
}
/// The mark type with upstream value `code`, if any.
#[must_use]
pub const fn from_code(code: i32) -> Option<Self> {
match code {
0 => Some(Self::NoMark),
1 => Some(Self::Mark),
2 => Some(Self::Mark2),
4 => Some(Self::Mark3),
8 => Some(Self::MarkIo),
16 => Some(Self::MarkAssert),
31 => Some(Self::AllMarks),
_ => None,
}
}
/// Index of the mark's counter and lock in a [`Context`]; `None` for
/// [`MarkType::NoMark`] and [`MarkType::AllMarks`].
const fn slot(self) -> Option<usize> {
match self {
Self::Mark => Some(0),
Self::Mark2 => Some(1),
Self::Mark3 => Some(2),
Self::MarkIo => Some(3),
Self::MarkAssert => Some(4),
Self::NoMark | Self::AllMarks => None,
}
}
}
/// Mark mask selecting every mark (`SM_MT_ALLMARKS`).
pub const ALL_MARKS: i32 = MarkType::AllMarks as i32;
/// Index of `SM_MT_MARKIO` in the mark arrays (the `default:` mark upstream).
const MARK_IO: usize = 3;
/// Receiver of topology-change notifications (`SmNotifyCallback`).
///
/// The notification method (`SmNotifyCallback::Execute`) is added by the unit that
/// ports `SmObject::Notify`.
pub trait NotifyCallback: std::fmt::Debug + Send {}
/// Shared modeling state (`SmContext`): traversal marks, the Boolean flag, notify
/// callbacks and the context's model size estimate and zone tolerance.
#[derive(Debug)]
pub struct Context {
/// `m_lCurrentMark`, `m_lCurrentMark2`, `m_lCurrentMark3`, `m_lCurrentMarkIO`,
/// `m_lCurrentMarkAssert`.
current_marks: [u64; 5],
/// `m_bMarkLock`, `m_bMark2Lock`, `m_bMark3Lock`, `m_bMarkIOLock`,
/// `m_bMarkAssertLock`.
mark_locks: [bool; 5],
/// `m_bDoingBoolean`.
doing_boolean: bool,
/// `m_dThisModelSizeEstimate`.
model_size_estimate: f64,
/// `m_dThisLargeSmallSizeRatio`.
large_small_size_ratio: f64,
/// `m_sThisZoneTol3d`.
zone_tol_3d: f64,
/// `m_pUserNotifyCallback`.
user_notify_callback: Option<Box<dyn NotifyCallback>>,
/// `m_pSysNotifyCallBack`.
sys_notify_callback: Option<Box<dyn NotifyCallback>>,
}
impl Default for Context {
/// `SmContext()`: both sizes [`tol::USE_DEFAULT`].
fn default() -> Self {
Self::new(tol::USE_DEFAULT, tol::USE_DEFAULT)
}
}
impl Context {
/// Creates a context (`SmContext::SmContext(double, double)`): every mark at 1 and
/// unlocked, not in a Boolean, no callbacks; the model size estimate and its zone
/// tolerance set by [`Context::set_this_model_size_estimate`]. Pass
/// [`tol::USE_DEFAULT`] for the system values.
#[must_use]
pub fn new(model_size_estimate: f64, large_small_size_ratio: f64) -> Self {
let mut c = Self {
current_marks: [1; 5],
mark_locks: [false; 5],
doing_boolean: false,
model_size_estimate: tol::USE_DEFAULT,
large_small_size_ratio: tol::USE_DEFAULT,
zone_tol_3d: tol::ZONE_TOL_3D,
user_notify_callback: None,
sys_notify_callback: None,
};
c.large_small_size_ratio = large_small_size_ratio;
c.set_this_model_size_estimate(model_size_estimate);
c
}
/// Whether a Boolean operation is running (`GetDoingBoolean`).
#[must_use]
pub const fn doing_boolean(&self) -> bool {
self.doing_boolean
}
/// Mutable access to the Boolean flag (`GetDoingBooleanRef`).
pub fn doing_boolean_mut(&mut self) -> &mut bool {
&mut self.doing_boolean
}
/// Sets the Boolean flag (`SetDoingBoolean`).
pub fn set_doing_boolean(&mut self, doing_boolean: bool) {
self.doing_boolean = doing_boolean;
}
/// The application's notify callback (`GetUserNotifyCallback`).
#[must_use]
pub fn user_notify_callback(&self) -> Option<&dyn NotifyCallback> {
self.user_notify_callback.as_deref()
}
/// Sets the application's notify callback (`SetUserNotifyCallback`).
pub fn set_user_notify_callback(&mut self, callback: Option<Box<dyn NotifyCallback>>) {
self.user_notify_callback = callback;
}
/// The system notify callback (`GetSysNotifyCallback`).
#[must_use]
pub fn sys_notify_callback(&self) -> Option<&dyn NotifyCallback> {
self.sys_notify_callback.as_deref()
}
/// Sets the system notify callback (`SetSysNotifyCallback`; upstream only warns
/// in debug builds when one is already set).
pub fn set_sys_notify_callback(&mut self, callback: Option<Box<dyn NotifyCallback>>) {
self.sys_notify_callback = callback;
}
/// The context's model size estimate, or the system one when it is
/// [`tol::USE_DEFAULT`] (`GetThisModelSizeEstimate`).
#[must_use]
pub fn this_model_size_estimate(&self, k: &Kernel) -> f64 {
if self.model_size_estimate == tol::USE_DEFAULT {
tol::model_size_estimate(k)
} else {
self.model_size_estimate
}
}
/// Stores a model size estimate and the zone tolerance it implies
/// ([`tol::size_estimate_to_zone_tol_3d`]); returns that zone tolerance
/// (`SetThisModelSizeEstimate`).
pub fn set_this_model_size_estimate(&mut self, model_size_estimate: f64) -> f64 {
self.model_size_estimate = model_size_estimate;
self.zone_tol_3d = tol::size_estimate_to_zone_tol_3d(self.model_size_estimate);
self.zone_tol_3d
}
/// Stores the large/small size ratio (`SetThisLargeSmallSizeRatio`).
pub fn set_this_large_small_size_ratio(&mut self, large_small_size_ratio: f64) {
self.large_small_size_ratio = large_small_size_ratio;
}
/// The stored model size estimate, possibly [`tol::USE_DEFAULT`]
/// (`m_dThisModelSizeEstimate`).
#[must_use]
pub const fn stored_model_size_estimate(&self) -> f64 {
self.model_size_estimate
}
/// The stored large/small size ratio, possibly [`tol::USE_DEFAULT`]
/// (`m_dThisLargeSmallSizeRatio`).
#[must_use]
pub const fn stored_large_small_size_ratio(&self) -> f64 {
self.large_small_size_ratio
}
/// The context's zone tolerance (`m_sThisZoneTol3d`).
#[must_use]
pub const fn this_zone_tol_3d(&self) -> f64 {
self.zone_tol_3d
}
/// The current value of a mark (`GetCurrentMark`): an object is marked when its
/// mark equals this. [`MarkType::NoMark`] gives one more than the largest mark, so
/// nothing is marked; [`MarkType::AllMarks`] gives the IO mark.
#[must_use]
pub fn current_mark(&self, mark_type: MarkType) -> u64 {
match mark_type {
MarkType::NoMark => {
let [m1, m2, m3, mio, massert] = self.current_marks;
1 + max5(m1, m2, m3, mio, massert)
}
_ => self
.current_marks
.get(mark_type.slot().unwrap_or(MARK_IO))
.copied()
.unwrap_or(0),
}
}
/// Advances a mark by two and returns the new value (`SmContext::NewMark`; the
/// lock check is a debug-only assertion upstream). [`MarkType::NoMark`] changes
/// nothing and returns [`Context::current_mark`] of it.
fn new_mark(&mut self, mark_type: MarkType) -> u64 {
if mark_type == MarkType::NoMark {
return self.current_mark(MarkType::NoMark);
}
match self
.current_marks
.get_mut(mark_type.slot().unwrap_or(MARK_IO))
{
Some(m) => {
*m = m.wrapping_add(2);
*m
}
None => 0,
}
}
/// Whether a mark is locked (`IsMarkLocked`); never for [`MarkType::NoMark`] or
/// [`MarkType::AllMarks`].
#[must_use]
pub fn is_mark_locked(&self, mark_type: MarkType) -> bool {
mark_type
.slot()
.and_then(|s| self.mark_locks.get(s))
.copied()
.unwrap_or(false)
}
/// Sets a mark's lock flag (no-op for marks without a slot).
fn set_lock(&mut self, mark_type: MarkType, locked: bool) {
if let Some(l) = mark_type.slot().and_then(|s| self.mark_locks.get_mut(s)) {
*l = locked;
}
}
}
/// `smos_5Max` = `smos_Max(smos_3Max(a, b, c), smos_Max(d, e))`.
fn max5(a: u64, b: u64, c: u64, d: u64, e: u64) -> u64 {
a.max(b).max(c).max(d.max(e))
}
/// `SmNewMarkAndLock::LockMark`.
fn lock_mark(ctx: &mut Context, mark_type: MarkType) {
ctx.set_lock(mark_type, true);
}
/// `SmNewMarkAndLock::UnlockMark`.
fn unlock_mark(ctx: &mut Context, mark_type: MarkType) {
ctx.set_lock(mark_type, false);
}
/// The first unlocked mark selected by `mask` in the order mark, mark2, mark3, IO,
/// assert; if all are locked, the mask itself when it names a single mark, else the IO
/// mark (`SmNewMarkAndLock::GetUnlockedMark`).
fn unlocked_mark(ctx: &Context, mask: i32) -> MarkType {
const ORDER: [MarkType; 5] = [
MarkType::Mark,
MarkType::Mark2,
MarkType::Mark3,
MarkType::MarkIo,
MarkType::MarkAssert,
];
if let Some(m) = ORDER
.iter()
.find(|m| (mask & m.code()) != 0 && !ctx.is_mark_locked(**m))
{
return *m;
}
ORDER
.iter()
.find(|m| mask == m.code())
.copied()
.unwrap_or(MarkType::MarkIo)
}
const OP_LOCK: &str = "geom::context::MarkLock";
/// The context at `index` in `contexts`.
fn ctx_at(contexts: &mut [Context], index: usize) -> Result<&mut Context> {
contexts.get_mut(index).ok_or(SolidError::InvalidInput {
operation: OP_LOCK,
reason: "context index out of range",
})
}
/// Fails when a bound context index is outside `contexts`.
fn check_index(contexts: &[Context], index: usize) -> Result<()> {
if index < contexts.len() {
Ok(())
} else {
Err(SolidError::InvalidInput {
operation: OP_LOCK,
reason: "context index out of range",
})
}
}
/// A claimed traversal mark (`SmNewMarkAndLock`): advances an unlocked mark of one or
/// two contexts and locks it until [`MarkLock::release`].
///
/// Contexts are given as indices into a caller-owned `&mut [Context]` (see the module
/// deviations); pass the same slice to every method.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MarkLock {
context1: Option<usize>,
context2: Option<usize>,
mark_type1: MarkType,
mark_type2: MarkType,
}
impl Default for MarkLock {
fn default() -> Self {
Self::unbound()
}
}
impl MarkLock {
/// A lock bound to no context (`SmNewMarkAndLock()`).
#[must_use]
pub const fn unbound() -> Self {
Self {
context1: None,
context2: None,
mark_type1: MarkType::NoMark,
mark_type2: MarkType::NoMark,
}
}
/// Picks the first unlocked mark of `contexts[ctx]` allowed by `mask` (usually
/// [`ALL_MARKS`]), advances and locks it (`SmNewMarkAndLock(const SmContext*,
/// SmBitArray)`).
pub fn new(contexts: &mut [Context], ctx: usize, mask: i32) -> Result<Self> {
let c = ctx_at(contexts, ctx)?;
let mark_type1 = unlocked_mark(c, mask);
c.new_mark(mark_type1);
lock_mark(c, mark_type1);
Ok(Self {
context1: Some(ctx),
context2: None,
mark_type1,
mark_type2: MarkType::NoMark,
})
}
/// Claims a mark in each of two contexts, e.g. the two operands of a Boolean
/// (`SmNewMarkAndLock(SmContext*, SmContext*)`). When `ctx1 == ctx2` both marks
/// are the same. Otherwise, as upstream, the second mark type is picked from
/// `contexts[ctx2]` but the first mark is advanced again in `contexts[ctx1]` and
/// the *first* mark type is locked in `contexts[ctx2]`.
pub fn new_pair(contexts: &mut [Context], ctx1: usize, ctx2: usize) -> Result<Self> {
check_index(contexts, ctx2)?;
let c1 = ctx_at(contexts, ctx1)?;
let mark_type1 = unlocked_mark(c1, ALL_MARKS);
c1.new_mark(mark_type1);
lock_mark(c1, mark_type1);
let mark_type2 = if ctx1 == ctx2 {
mark_type1
} else {
let m2 = unlocked_mark(ctx_at(contexts, ctx2)?, ALL_MARKS);
ctx_at(contexts, ctx1)?.new_mark(mark_type1);
lock_mark(ctx_at(contexts, ctx2)?, mark_type1);
m2
};
Ok(Self {
context1: Some(ctx1),
context2: Some(ctx2),
mark_type1,
mark_type2,
})
}
/// Unlocks the claimed marks that are still locked and unbinds the lock
/// (`~SmNewMarkAndLock`).
pub fn release(&mut self, contexts: &mut [Context]) -> Result<()> {
if let Some(i) = self.context1 {
let c = ctx_at(contexts, i)?;
if c.is_mark_locked(self.mark_type1) {
unlock_mark(c, self.mark_type1);
}
}
if let Some(i) = self.context2 {
let c = ctx_at(contexts, i)?;
if c.is_mark_locked(self.mark_type2) {
unlock_mark(c, self.mark_type2);
}
}
*self = Self::unbound();
Ok(())
}
/// The (first) claimed mark (`GetMarkType`).
#[must_use]
pub const fn mark_type(&self) -> MarkType {
self.mark_type1
}
/// The mark claimed in the first context (`GetMarkType1`).
#[must_use]
pub const fn mark_type1(&self) -> MarkType {
self.mark_type1
}
/// The mark claimed in the second context (`GetMarkType2`).
#[must_use]
pub const fn mark_type2(&self) -> MarkType {
self.mark_type2
}
/// Index of the (first) bound context (`GetContext`).
#[must_use]
pub const fn context(&self) -> Option<usize> {
self.context1
}
/// Index of the second bound context, if any (`GetContext2`).
#[must_use]
pub const fn context2(&self) -> Option<usize> {
self.context2
}
/// Advances the claimed mark(s) while keeping them locked, invalidating every
/// mark set so far (`SmNewMarkAndLock::NewMark`).
pub fn new_mark(&self, contexts: &mut [Context]) -> Result<()> {
let i1 = self.context1.ok_or(SolidError::InvalidInput {
operation: "geom::context::MarkLock::new_mark",
reason: "no context bound",
})?;
ctx_at(contexts, i1)?.new_mark(self.mark_type1);
if let Some(i2) = self.context2 {
if i1 != i2 {
ctx_at(contexts, i2)?.new_mark(self.mark_type2);
}
}
Ok(())
}
/// Rebinds the first context: unlocks the old first mark, then claims and locks
/// (without advancing) the first unlocked mark of `contexts[ctx]` allowed by
/// `mask`; returns it (`SmNewMarkAndLock::SetContext(SmContext*, SmMarkType)`).
/// The second context binding is kept.
pub fn set_context(
&mut self,
contexts: &mut [Context],
ctx: usize,
mask: i32,
) -> Result<MarkType> {
check_index(contexts, ctx)?;
if let Some(i) = self.context1 {
if self.mark_type1 != MarkType::NoMark {
unlock_mark(ctx_at(contexts, i)?, self.mark_type1);
}
}
self.context1 = Some(ctx);
let c = ctx_at(contexts, ctx)?;
self.mark_type1 = unlocked_mark(c, mask);
lock_mark(c, self.mark_type1);
Ok(self.mark_type1)
}
/// Rebinds both contexts: unlocks the old marks, then claims and locks (without
/// advancing) the first unlocked mark of each new context; returns the first
/// (`SmNewMarkAndLock::SetContext(SmContext*, SmContext*)`).
pub fn set_context_pair(
&mut self,
contexts: &mut [Context],
ctx1: usize,
ctx2: usize,
) -> Result<MarkType> {
check_index(contexts, ctx1)?;
check_index(contexts, ctx2)?;
if let Some(i) = self.context1 {
if self.mark_type1 != MarkType::NoMark {
unlock_mark(ctx_at(contexts, i)?, self.mark_type1);
}
}
if let Some(i2) = self.context2 {
if self.context1 != Some(i2) && self.mark_type2 != MarkType::NoMark {
unlock_mark(ctx_at(contexts, i2)?, self.mark_type2);
}
}
self.context1 = Some(ctx1);
self.context2 = Some(ctx2);
let c1 = ctx_at(contexts, ctx1)?;
self.mark_type1 = unlocked_mark(c1, ALL_MARKS);
lock_mark(c1, self.mark_type1);
if ctx1 == ctx2 {
self.mark_type2 = self.mark_type1;
} else {
let c2 = ctx_at(contexts, ctx2)?;
self.mark_type2 = unlocked_mark(c2, ALL_MARKS);
lock_mark(c2, self.mark_type2);
}
Ok(self.mark_type1)
}
}
/// Runs `f` with a mark of the kernel's context claimed and locked, then releases it:
/// the scoped `{ SmNewMarkAndLock sMarkLock(pContext, mask); ... }` idiom.
pub fn with_mark_lock<R>(
k: &mut Kernel,
mask: i32,
f: impl FnOnce(&mut Kernel, MarkType) -> R,
) -> Result<R> {
let mut lock = MarkLock::new(std::slice::from_mut(&mut k.context), 0, mask)?;
let r = f(k, lock.mark_type());
lock.release(std::slice::from_mut(&mut k.context))?;
Ok(r)
}
/// How mass-property computations treat faces, edges and vertices
/// (`SmMassPropertyBehavior`).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MassPropertyBehavior {
/// Density of the material (`m_dDensity`, default 1).
pub density: f64,
/// Thickness given to faces (`m_dFaceThickness`, default 0).
pub face_thickness: f64,
/// Radius of the tube swept around edges (`m_dEdgeCrossSectionRadius`, default 0).
pub edge_cross_section_radius: f64,
/// Radius of the sphere around vertices (`m_dVertexSphereRadius`, default 0).
pub vertex_sphere_radius: f64,
/// Face area mode (`m_lFaceAreaBehavior`): 0 solid/void faces only, 1 every face
/// once, 2 every face twice.
pub face_area_behavior: u64,
/// Face volume mode (`m_lFaceVolumeBehavior`): 0 region volumes, 1 thin shells.
pub face_volume_behavior: u64,
}
impl Default for MassPropertyBehavior {
fn default() -> Self {
Self::new(1.0, 0.0, 0.0, 0.0, 0, 0)
}
}
impl MassPropertyBehavior {
/// Builds the options (`SmMassPropertyBehavior::SmMassPropertyBehavior`).
#[must_use]
pub const fn new(
density: f64,
face_thickness: f64,
edge_cross_section_radius: f64,
vertex_sphere_radius: f64,
face_area_behavior: u64,
face_volume_behavior: u64,
) -> Self {
Self {
density,
face_thickness,
edge_cross_section_radius,
vertex_sphere_radius,
face_area_behavior,
face_volume_behavior,
}
}
}
/// Creates the default context in `slot` unless one exists (`SmApiCreateContext`).
pub fn create_context(slot: &mut Option<Context>) {
if slot.is_none() {
*slot = Some(Context::default());
}
}
/// The context in `slot`, created with defaults first if empty
/// (`SmApiGetOrCreateContext`).
pub fn get_or_create_context(slot: &mut Option<Context>) -> &mut Context {
slot.get_or_insert_with(Context::default)
}
#[cfg(test)]
mod tests {
use super::*;
const ALL: [MarkType; 7] = [
MarkType::NoMark,
MarkType::Mark,
MarkType::Mark2,
MarkType::Mark3,
MarkType::MarkIo,
MarkType::MarkAssert,
MarkType::AllMarks,
];
#[test]
fn mark_type_codes_round_trip() {
for m in ALL {
assert_eq!(MarkType::from_code(m.code()), Some(m));
}
assert_eq!(MarkType::from_code(3), None);
}
#[test]
fn new_context_defaults() {
let k = Kernel::default();
let c = Context::default();
for m in ALL {
assert!(!c.is_mark_locked(m));
}
assert_eq!(c.current_mark(MarkType::Mark), 1);
assert_eq!(c.current_mark(MarkType::NoMark), 2);
assert_eq!(c.current_mark(MarkType::AllMarks), 1);
assert!(!c.doing_boolean());
assert_eq!(c.this_zone_tol_3d(), 1.0e-5);
assert_eq!(c.this_model_size_estimate(&k), 50.0);
assert!(c.user_notify_callback().is_none());
assert!(c.sys_notify_callback().is_none());
}
#[test]
fn model_size_sets_zone_tol() {
let k = Kernel::default();
let mut c = Context::new(1000.0, tol::USE_DEFAULT);
assert_eq!(c.this_zone_tol_3d(), 1.0e-4);
assert_eq!(c.this_model_size_estimate(&k), 1000.0);
assert_eq!(c.set_this_model_size_estimate(0.01), 1.0e-6);
assert_eq!(c.set_this_model_size_estimate(tol::USE_DEFAULT), 1.0e-5);
assert_eq!(c.this_model_size_estimate(&k), 50.0);
c.set_this_large_small_size_ratio(10.0);
*c.doing_boolean_mut() = true;
assert!(c.doing_boolean());
}
#[test]
fn single_lock_cycle() {
let mut cs = [Context::default()];
let mut a = MarkLock::new(&mut cs, 0, ALL_MARKS).unwrap();
assert_eq!(a.mark_type(), MarkType::Mark);
assert_eq!(cs[0].current_mark(MarkType::Mark), 3);
assert!(cs[0].is_mark_locked(MarkType::Mark));
let mut b = MarkLock::new(&mut cs, 0, ALL_MARKS).unwrap();
assert_eq!(b.mark_type(), MarkType::Mark2);
b.new_mark(&mut cs).unwrap();
assert_eq!(cs[0].current_mark(MarkType::Mark2), 5);
b.release(&mut cs).unwrap();
a.release(&mut cs).unwrap();
assert!(!cs[0].is_mark_locked(MarkType::Mark));
assert!(!cs[0].is_mark_locked(MarkType::Mark2));
assert_eq!(cs[0].current_mark(MarkType::NoMark), 6);
assert!(MarkLock::unbound().new_mark(&mut cs).is_err());
}
#[test]
fn exhausted_marks_fall_back() {
let mut cs = [Context::default()];
let locks: Vec<MarkLock> = (0..5)
.map(|_| MarkLock::new(&mut cs, 0, ALL_MARKS).unwrap())
.collect();
assert_eq!(locks[4].mark_type(), MarkType::MarkAssert);
let x = MarkLock::new(&mut cs, 0, ALL_MARKS).unwrap();
assert_eq!(x.mark_type(), MarkType::MarkIo);
let y = MarkLock::new(&mut cs, 0, MarkType::Mark3.code()).unwrap();
assert_eq!(y.mark_type(), MarkType::Mark3);
}
#[test]
fn pair_lock_upstream_quirk() {
let mut cs = [Context::default(), Context::default()];
let _first = MarkLock::new(&mut cs, 1, ALL_MARKS).unwrap();
let mut p = MarkLock::new_pair(&mut cs, 0, 1).unwrap();
assert_eq!(p.mark_type1(), MarkType::Mark);
assert_eq!(p.mark_type2(), MarkType::Mark2);
// context 0's mark advanced twice; context 1 gets mark type 1 locked
assert_eq!(cs[0].current_mark(MarkType::Mark), 5);
assert!(cs[1].is_mark_locked(MarkType::Mark));
assert!(!cs[1].is_mark_locked(MarkType::Mark2));
p.release(&mut cs).unwrap();
assert!(!cs[0].is_mark_locked(MarkType::Mark));
assert!(cs[1].is_mark_locked(MarkType::Mark));
let same = MarkLock::new_pair(&mut cs, 0, 0).unwrap();
assert_eq!(same.mark_type2(), same.mark_type1());
assert!(MarkLock::new_pair(&mut cs, 0, 2).is_err());
}
#[test]
fn set_context_rebinds() {
let mut cs = [Context::default(), Context::default()];
let mut l = MarkLock::unbound();
assert_eq!(
l.set_context(&mut cs, 1, ALL_MARKS).unwrap(),
MarkType::Mark
);
assert_eq!(cs[1].current_mark(MarkType::Mark), 1);
assert_eq!(l.set_context_pair(&mut cs, 0, 1).unwrap(), MarkType::Mark);
assert!(cs[0].is_mark_locked(MarkType::Mark));
assert!(cs[1].is_mark_locked(MarkType::Mark));
l.release(&mut cs).unwrap();
assert!(!cs[0].is_mark_locked(MarkType::Mark));
assert!(!cs[1].is_mark_locked(MarkType::Mark));
}
#[test]
fn scoped_kernel_lock() {
let mut k = Kernel::default();
let m = with_mark_lock(&mut k, ALL_MARKS, |k, m| {
assert!(k.context.is_mark_locked(m));
m
})
.unwrap();
assert_eq!(m, MarkType::Mark);
assert!(!k.context.is_mark_locked(m));
}
#[test]
fn api_context_slot() {
let mut slot = None;
create_context(&mut slot);
get_or_create_context(&mut slot).set_doing_boolean(true);
create_context(&mut slot);
assert!(get_or_create_context(&mut slot).doing_boolean());
let mp = MassPropertyBehavior::default();
assert_eq!(mp.density, 1.0);
assert_eq!(mp.face_area_behavior, 0);
}
}
+63 -33
View File
@@ -23,6 +23,8 @@
//! | [`xsect_tol_3d_from_zones`] | `SmTol::GetXSectTol3d(const SmZoneTol3d&, const SmZoneTol3d&)` |
//! | [`ang_tol_rad`] | `SmTol::GetAngTolRad()` |
//! | [`model_size_estimate`] | `SmTol::GetModelSizeEstimate()` |
//! | [`context_model_size_estimate`] | `SmTol::GetModelSizeEstimate(const SmContext&)` |
//! | [`set_context_model_size_estimate`] | `SmTol::SetModelSizeEstimate(SmContext&, double)` |
//! | [`are_parallel`] | `SmTol::AreParallel` |
//! | [`are_perpendicular`] | `SmTol::ArePerpendicular` |
//! | [`is_zero_speed`] | `SmTol::IsZeroSpeed` |
@@ -56,17 +58,15 @@
//! System state: upstream keeps the system tolerances in thread-local statics
//! (`s_ZoneTol3d`, `s_TolGapGain`, `s_ModelSizeEstimate`, `s_LargeSmallSizeRatio`);
//! here they are the [`Tolerances`] of the [`Kernel`] (`zone_tol_3d`, `tol_gap_gain`,
//! `model_size_estimate`, `large_small_size_ratio`). The kernel also plays the
//! `SmContext`, so the `const SmContext*` overloads are the `context_*` functions
//! with a null context (the context's own `m_sThisZoneTol3d` is the kernel's
//! `zone_tol_3d` as well). Note that the context overloads ignore the small-topology
//! `model_size_estimate`, `large_small_size_ratio`). The `const SmContext*` overloads
//! take an `Option<&`[`Context`]`>` (`None` = null pointer: the system value; usually
//! `Some(&k.context)`). Note that the context overloads ignore the small-topology
//! flag, as upstream does.
//!
//! Not here yet (they need types of later units; see the `UNPORTED` markers below):
//! every overload taking an `SmObject`, `SmCurve`, `SmSurface`, `SmVolume`,
//! `SmPseudoBox`, `SmExtent1d`, `SmExtent2d`, `SmBrep`, `SmBrepData`, a topology
//! entity, a classification / interval object, `SmEvalNFunctionsObject`, or a
//! non-null `SmContext` with its own tolerances.
//! entity, a classification / interval object, or `SmEvalNFunctionsObject`.
//!
//! Deviations:
//! - The tolerance classes (`SmTol3d`, `SmZoneTol3d`, ...) are plain `f64` values; their
@@ -93,15 +93,12 @@
use std::collections::{BTreeMap, BTreeSet};
use crate::error::{Result, SolidError};
use crate::geom::context::Context;
use crate::geom::extent3d::Extent3d;
use crate::geom::vector2d::Vec2;
use crate::kernel::{Kernel, Tolerances};
use crate::math::{Vec3, EFF_ZERO, EFF_ZERO_PARAM, EFF_ZERO_RAD, EFF_ZERO_SQ, EFF_ZERO_SQRT, PI};
// UNPORTED(geom.context): SmTol::GetModelSizeEstimate(const SmContext&),
// SmTol::SetModelSizeEstimate(SmContext&, double) and the non-null-context branch of
// SmTol::GetZoneTol3d(const SmContext*, ...) need a context with its own
// m_dThisModelSizeEstimate / m_sThisZoneTol3d (SmContext::SetThisModelSizeEstimate).
// UNPORTED(topology.brep_p1): SmTol::GetModelSizeEstimate(const SmBrep&),
// SmTol::SetModelSizeEstimate(SmBrep&, double), SmTol::GetZoneTol3d(const SmBrep*),
// SmTol::GetApproxTol3d(const SmBrep*), SmTol::CalcFaceZoneTol3d(SmBrep*),
@@ -361,6 +358,19 @@ pub fn model_size_estimate(k: &Kernel) -> f64 {
k.tolerances.model_size_estimate
}
/// A context's model size estimate, the system one if it has none
/// (`SmTol::GetModelSizeEstimate(const SmContext&)`).
#[must_use]
pub fn context_model_size_estimate(k: &Kernel, ctx: &Context) -> f64 {
ctx.this_model_size_estimate(k)
}
/// Sets a context's model size estimate and returns its new zone tolerance
/// (`SmTol::SetModelSizeEstimate(SmContext&, double)`).
pub fn set_context_model_size_estimate(ctx: &mut Context, model_size_estimate: f64) -> f64 {
ctx.set_this_model_size_estimate(model_size_estimate)
}
/// The gap gain: the over-size factor that keeps an object's zone tolerance a little
/// larger than half its largest gap (`SmTol::GetGapGain`).
#[must_use]
@@ -384,16 +394,16 @@ pub fn zone_tol_3d(k: &Kernel, small: SmallTopoType) -> f64 {
}
/// The context zone tolerance (`SmTol::GetZoneTol3d(const SmContext*,
/// SmSmallTopoType)` with a null context); `small` is ignored, as upstream.
/// SmSmallTopoType)`): the context's own zone tolerance, or the system one for `None`
/// (a null context); `small` is ignored, as upstream.
#[must_use]
pub fn context_zone_tol_3d(k: &Kernel, small: SmallTopoType) -> f64 {
pub fn context_zone_tol_3d(k: &Kernel, ctx: Option<&Context>, small: SmallTopoType) -> f64 {
let _ = small;
let z = k.tolerances.zone_tol_3d;
if z == UNINIT_TOL {
zone_tol3d(k, UNINIT_TOL)
} else {
// UNPORTED(geom.context): cpContext->m_sThisZoneTol3d for a non-null context.
z
ctx.map_or(z, Context::this_zone_tol_3d)
}
}
@@ -405,10 +415,10 @@ pub fn approx_tol_3d(k: &Kernel, small: SmallTopoType) -> f64 {
}
/// The context approximation tolerance (`SmTol::GetApproxTol3d(const SmContext*,
/// SmSmallTopoType)` with a null context).
/// SmSmallTopoType)`; `None` = null context).
#[must_use]
pub fn context_approx_tol_3d(k: &Kernel, small: SmallTopoType) -> f64 {
approx_tol3d(k, 0.5 * context_zone_tol_3d(k, small))
pub fn context_approx_tol_3d(k: &Kernel, ctx: Option<&Context>, small: SmallTopoType) -> f64 {
approx_tol3d(k, 0.5 * context_zone_tol_3d(k, ctx, small))
}
/// The system intersection tolerance, twice the zone tolerance
@@ -419,10 +429,10 @@ pub fn xsect_tol_3d(k: &Kernel, small: SmallTopoType) -> f64 {
}
/// The context intersection tolerance (`SmTol::GetXSectTol3d(const SmContext*,
/// SmSmallTopoType)` with a null context).
/// SmSmallTopoType)`; `None` = null context).
#[must_use]
pub fn context_xsect_tol_3d(k: &Kernel, small: SmallTopoType) -> f64 {
xsect_tol3d(k, 2.0 * context_zone_tol_3d(k, small))
pub fn context_xsect_tol_3d(k: &Kernel, ctx: Option<&Context>, small: SmallTopoType) -> f64 {
xsect_tol3d(k, 2.0 * context_zone_tol_3d(k, ctx, small))
}
/// The intersection tolerance of two objects with zone tolerances `zone1`, `zone2`:
@@ -466,14 +476,15 @@ pub fn eff_zero_ang_rad() -> f64 {
// ---- topology zone tolerances from gaps --------------------------------------------
/// A face's zone tolerance: the context zone tolerance
/// (`SmTol::CalcFaceZoneTol3d(const SmContext*)` with a null context).
/// (`SmTol::CalcFaceZoneTol3d(const SmContext*)`; `None` = null context).
#[must_use]
pub fn calc_face_zone_tol_3d(k: &Kernel) -> f64 {
context_zone_tol_3d(k, SmallTopoType::NormalSize)
pub fn calc_face_zone_tol_3d(k: &Kernel, ctx: Option<&Context>) -> f64 {
context_zone_tol_3d(k, ctx, SmallTopoType::NormalSize)
}
/// An edge's zone tolerance from its largest edge/edge and edge/face gaps
/// (`SmTol::CalcEdgeZoneTol3d` with a null context):
/// (`SmTol::CalcEdgeZoneTol3d(double, double, SmZoneTol3d*, const SmContext*)`;
/// `None` = null context):
/// `max(def, max_face_zone_tol?, gain * edge_edge_gap / 2, gain * edge_face_gap - def)`
/// where `def` is the context zone tolerance and `gain` the [`gap_gain`].
#[must_use]
@@ -482,12 +493,20 @@ pub fn calc_edge_zone_tol_3d(
edge_edge_max_gap: f64,
edge_face_max_gap: f64,
max_face_zone_tol: Option<f64>,
ctx: Option<&Context>,
) -> f64 {
calc_gap_zone_tol(k, edge_edge_max_gap, edge_face_max_gap, max_face_zone_tol)
calc_gap_zone_tol(
k,
edge_edge_max_gap,
edge_face_max_gap,
max_face_zone_tol,
ctx,
)
}
/// A vertex's zone tolerance from its largest vertex/edge and vertex/face gaps
/// (`SmTol::CalcVertexZoneTol3d` with a null context); same formula as
/// (`SmTol::CalcVertexZoneTol3d(double, double, SmZoneTol3d*, const SmContext*)`;
/// `None` = null context); same formula as
/// [`calc_edge_zone_tol_3d`].
#[must_use]
pub fn calc_vertex_zone_tol_3d(
@@ -495,18 +514,26 @@ pub fn calc_vertex_zone_tol_3d(
vertex_edge_max_gap: f64,
vertex_face_max_gap: f64,
max_edge_zone_tol: Option<f64>,
ctx: Option<&Context>,
) -> f64 {
calc_gap_zone_tol(
k,
vertex_edge_max_gap,
vertex_face_max_gap,
max_edge_zone_tol,
ctx,
)
}
/// Shared body of `CalcEdgeZoneTol3d` / `CalcVertexZoneTol3d`.
fn calc_gap_zone_tol(k: &Kernel, gap_1d: f64, gap_2d: f64, max_up: Option<f64>) -> f64 {
let def = context_zone_tol_3d(k, SmallTopoType::NormalSize);
fn calc_gap_zone_tol(
k: &Kernel,
gap_1d: f64,
gap_2d: f64,
max_up: Option<f64>,
ctx: Option<&Context>,
) -> f64 {
let def = context_zone_tol_3d(k, ctx, SmallTopoType::NormalSize);
let gain = gap_gain(k);
let r = match max_up {
Some(up) => smax4(def, up, gain * gap_1d / 2.0, gain * gap_2d - def),
@@ -956,7 +983,7 @@ mod tests {
assert_eq!(approx_tol_3d(&k, SmallTopoType::NormalSize), 5.0e-6);
assert_eq!(zone_tol_3d(&k, SmallTopoType::SmallSize), 1000.0 * 1.0e-5);
assert_eq!(
context_zone_tol_3d(&k, SmallTopoType::ForceSmallSize),
context_zone_tol_3d(&k, None, SmallTopoType::ForceSmallSize),
1.0e-5
);
assert_eq!(gap_gain(&k), 1.1);
@@ -979,10 +1006,13 @@ mod tests {
#[test]
fn gap_zone_tolerances() {
let k = Kernel::default();
assert_eq!(calc_edge_zone_tol_3d(&k, 0.0, 0.0, None), 1.0e-5);
assert_eq!(calc_edge_zone_tol_3d(&k, 2.0, 0.0, None), 1.1);
assert_eq!(calc_vertex_zone_tol_3d(&k, 0.0, 0.0, Some(0.25)), 0.25);
assert_eq!(calc_face_zone_tol_3d(&k), 1.0e-5);
assert_eq!(calc_edge_zone_tol_3d(&k, 0.0, 0.0, None, None), 1.0e-5);
assert_eq!(calc_edge_zone_tol_3d(&k, 2.0, 0.0, None, None), 1.1);
assert_eq!(
calc_vertex_zone_tol_3d(&k, 0.0, 0.0, Some(0.25), None),
0.25
);
assert_eq!(calc_face_zone_tol_3d(&k, None), 1.0e-5);
}
#[test]
+7
View File
@@ -8,6 +8,7 @@
//! | Rust | Upstream |
//! |----------------------------------------|-------------------------------------------|
//! | [`Kernel`] | `SmContext` |
//! | [`Kernel::context`] | `SmContext` marks, flags and own tolerance (see [`crate::geom::context`]) |
//! | [`Tolerances::zone_tol_3d`] | `SmContext::m_sThisZoneTol3d`, `SM_ZONE_TOL_3D` |
//! | [`Tolerances::xsect_tol_3d`] | `SM_XSECT_TOL_3D` |
//! | [`Tolerances::approx_tol_3d`] | `SM_APPROX_TOL_3D` |
@@ -24,6 +25,8 @@
//! code must compare outcomes that are invariant to the draw (and say so in
//! `plan/findings.md`).
use crate::geom::context::Context;
/// Geometric tolerances in model units (upstream defaults from `SmMath.h`).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tolerances {
@@ -155,6 +158,9 @@ pub struct Kernel {
pub rng: Rng,
/// Memoisation caches.
pub caches: Caches,
/// The session's modeling context (marks, Boolean flag, callbacks, its own
/// model size estimate); created with the defaults, as `SmApiGetOrCreateContext`.
pub context: Context,
}
impl Default for Kernel {
@@ -171,6 +177,7 @@ impl Kernel {
tolerances: options.tolerances,
rng: Rng::new(options.seed),
caches: Caches::default(),
context: Context::default(),
}
}
}
+604
View File
@@ -0,0 +1,604 @@
{
"cases": [
{
"name": "default_context",
"upstream_ref": "tests/prog_test/src/smFeatureTest.cpp:26 (SmContext sContext)",
"inputs": {
"steps": [
{
"op": 0,
"ctx": 0,
"m": 0
},
{
"op": 0,
"ctx": 0,
"m": 1
},
{
"op": 0,
"ctx": 0,
"m": 2
},
{
"op": 0,
"ctx": 0,
"m": 4
},
{
"op": 0,
"ctx": 0,
"m": 8
},
{
"op": 0,
"ctx": 0,
"m": 16
},
{
"op": 0,
"ctx": 0,
"m": 31
},
{
"op": 1,
"ctx": 0,
"m": 0
},
{
"op": 1,
"ctx": 0,
"m": 1
},
{
"op": 1,
"ctx": 0,
"m": 2
},
{
"op": 1,
"ctx": 0,
"m": 4
},
{
"op": 1,
"ctx": 0,
"m": 8
},
{
"op": 1,
"ctx": 0,
"m": 16
},
{
"op": 1,
"ctx": 0,
"m": 31
},
{
"op": 11
},
{
"op": 14,
"m": 0
},
{
"op": 14,
"m": 2
},
{
"op": 14,
"ctx": -1,
"m": 0
}
]
}
},
{
"name": "api_create_context",
"upstream_ref": "tests/SM_API_test/src/TestSmBrep.cpp:45 (SmApiCreateContext)",
"inputs": {
"steps": [
{
"op": 19
},
{
"op": 19
}
]
}
},
{
"name": "scoped_lock_all_marks",
"upstream_ref": "source/SMLib/src/SmAssembly.cpp:275 (SmNewMarkAndLock sMarkLock(GetContext(), SM_MT_ALLMARKS))",
"inputs": {
"steps": [
{
"op": 2,
"slot": 0,
"m": 31
},
{
"op": 9,
"slot": 0
},
{
"op": 5,
"slot": 0
},
{
"op": 5,
"slot": 0
},
{
"op": 0,
"m": 1
},
{
"op": 8,
"slot": 0
},
{
"op": 1,
"m": 1
}
]
}
},
{
"name": "nested_locks_exhaust",
"upstream_ref": "source/SMLib/inc/SmContext.h:496 (GetUnlockedMark: all marks locked -> SM_MT_MARKIO)",
"inputs": {
"slots": 8,
"steps": [
{
"op": 2,
"slot": 0,
"m": 31
},
{
"op": 2,
"slot": 1,
"m": 31
},
{
"op": 2,
"slot": 2,
"m": 31
},
{
"op": 2,
"slot": 3,
"m": 31
},
{
"op": 2,
"slot": 4,
"m": 31
},
{
"op": 2,
"slot": 5,
"m": 31
},
{
"op": 2,
"slot": 6,
"m": 31
},
{
"op": 9,
"slot": 0
},
{
"op": 9,
"slot": 1
},
{
"op": 9,
"slot": 2
},
{
"op": 9,
"slot": 3
},
{
"op": 9,
"slot": 4
},
{
"op": 9,
"slot": 5
},
{
"op": 9,
"slot": 6
},
{
"op": 8,
"slot": 3
},
{
"op": 2,
"slot": 7,
"m": 31
},
{
"op": 9,
"slot": 7
}
]
}
},
{
"name": "lock_single_mask_locked",
"upstream_ref": "source/SMLib/src/SmContext.cpp:516 (GetUnlockedMark returns the single-mark mask)",
"inputs": {
"steps": [
{
"op": 2,
"slot": 0,
"m": 4
},
{
"op": 2,
"slot": 1,
"m": 4
},
{
"op": 9,
"slot": 1
},
{
"op": 2,
"slot": 2,
"m": 0
},
{
"op": 9,
"slot": 2
},
{
"op": 2,
"slot": 3,
"m": 3
},
{
"op": 9,
"slot": 3
}
]
}
},
{
"name": "default_lock_set_context",
"upstream_ref": "source/SMLib/src/SmSphere.cpp:3600 (SmNewMarkAndLock sMarkLock; SetContext(GetContext()))",
"inputs": {
"steps": [
{
"op": 4,
"slot": 0
},
{
"op": 9,
"slot": 0
},
{
"op": 5,
"slot": 0
},
{
"op": 6,
"slot": 0,
"m": 31
},
{
"op": 9,
"slot": 0
},
{
"op": 6,
"slot": 0,
"m": 2
},
{
"op": 9,
"slot": 0
}
]
}
},
{
"name": "boolean_pair_same_context",
"upstream_ref": "source/SMLib/src/SmPrimitiveCreation.cpp:7745 (SmNewMarkAndLock(BrepA ctx, BrepB ctx))",
"inputs": {
"steps": [
{
"op": 3,
"slot": 0,
"ctx": 0,
"ctx2": 0
},
{
"op": 9,
"slot": 0
},
{
"op": 5,
"slot": 0
},
{
"op": 10,
"m": 1
},
{
"op": 10,
"m": 0,
"flag": 1
}
]
}
},
{
"name": "boolean_pair_two_contexts",
"upstream_ref": "source/SMLib/src/SmPrimitiveCreation.cpp:7745 (SmNewMarkAndLock with distinct contexts)",
"inputs": {
"contexts": [
[
9876543.21,
9876543.21
],
[
9876543.21,
9876543.21
]
],
"steps": [
{
"op": 2,
"slot": 1,
"ctx": 1,
"m": 31
},
{
"op": 3,
"slot": 0,
"ctx": 0,
"ctx2": 1
},
{
"op": 9,
"slot": 0
},
{
"op": 5,
"slot": 0
},
{
"op": 8,
"slot": 0
},
{
"op": 7,
"slot": 2,
"ctx": 1,
"ctx2": 0
},
{
"op": 9,
"slot": 2
}
]
}
},
{
"name": "set_context_pair_rebind",
"upstream_ref": "source/SMLib/inc/SmContext.h:600 (SmNewMarkAndLock::SetContext(ctx1, ctx2))",
"inputs": {
"contexts": [
[
9876543.21,
9876543.21
],
[
9876543.21,
9876543.21
],
[
9876543.21,
9876543.21
]
],
"steps": [
{
"op": 7,
"slot": 0,
"ctx": 0,
"ctx2": 1
},
{
"op": 7,
"slot": 0,
"ctx": 2,
"ctx2": 2
},
{
"op": 6,
"slot": 0,
"ctx": 1,
"m": 31
},
{
"op": 9,
"slot": 0
},
{
"op": 5,
"slot": 0
}
]
}
},
{
"name": "model_size_1000",
"upstream_ref": "source/SMLib/src/SmContext.cpp:161 (SmContext(dThisModelSizeEstimate, dThisLargeSmallSizeRatio))",
"inputs": {
"contexts": [
[
1000.0,
50.0
],
[
0.01,
9876543.21
]
],
"steps": [
{
"op": 11,
"ctx": 0
},
{
"op": 11,
"ctx": 1
},
{
"op": 14,
"ctx": 0,
"m": 1
},
{
"op": 14,
"ctx": 1,
"m": 3
},
{
"op": 15,
"ctx": 0,
"x": [
0.0002,
0.0001,
0.0003,
0
],
"flag": 1
},
{
"op": 16,
"ctx": 1,
"x": [
2e-06,
0,
0,
0
]
}
]
}
},
{
"name": "set_model_size",
"upstream_ref": "source/SMLib/src/SmTol.cpp:80 (SmTol::SetModelSizeEstimate(SmContext&, double))",
"inputs": {
"sys_model": 5000.0,
"steps": [
{
"op": 11
},
{
"op": 12,
"x": [
0.001,
0,
0,
0
]
},
{
"op": 11
},
{
"op": 12,
"flag": 1,
"x": [
9876543.21,
0,
0,
0
]
},
{
"op": 11
},
{
"op": 13,
"x": [
10.0,
0,
0,
0
]
},
{
"op": 14,
"m": 4
},
{
"op": 14,
"ctx": -1,
"m": 2
}
]
}
},
{
"name": "notify_callbacks",
"upstream_ref": "source/SMLib/inc/SmContext.h:372 (SetSysNotifyCallback / GetUserNotifyCallback)",
"inputs": {
"steps": [
{
"op": 17,
"m": 1
},
{
"op": 17,
"m": 0
},
{
"op": 17,
"m": 1,
"flag": 1
},
{
"op": 17,
"m": 0,
"flag": 1
}
]
}
},
{
"name": "mass_property_behavior",
"upstream_ref": "source/SMLib/inc/SmTopoTypes.h:280 (SmMassPropertyBehavior)",
"inputs": {
"steps": [
{
"op": 18,
"slot": 1
},
{
"op": 18,
"x": [
7.8,
0.1,
0.2,
0.3
],
"m": 2,
"flag": 1
}
]
}
}
]
}
+328
View File
@@ -0,0 +1,328 @@
// SPDX-License-Identifier: Apache-2.0
// difftest shim for unit `geom.context`. Wraps upstream SmContext, SmNewMarkAndLock
// (source/SMLib/inc/SmContext.h, source/SMLib/src/SmContext.cpp), the SmContext
// overloads of SmTol (source/SMLib/src/SmTol.cpp), SmMassPropertyBehavior
// (source/SMLib/inc/SmTopoTypes.h) and SmApiCreateContext / SmApiGetOrCreateContext
// (source/SM_API/src/SmApiGeneral.cpp).
#include "sc_shim.h"
#include <SmApiGeneral.h>
#include <SmContext.h>
#include <SmTol.h>
#include <SmTopoTypes.h>
#include <cstring>
#include <vector>
namespace
{
// Exposes SmContext's protected tolerance members and accessors.
struct ScCtx : public SmContext
{
ScCtx(double dModel, double dRatio) : SmContext(dModel, dRatio) {}
using SmContext::GetThisModelSizeEstimate;
using SmContext::SetThisLargeSmallSizeRatio;
using SmContext::SetThisModelSizeEstimate;
double RawModel() const { return m_dThisModelSizeEstimate; }
double RawRatio() const { return m_dThisLargeSmallSizeRatio; }
double RawZone() const { return m_sThisZoneTol3d; }
};
// A concrete notify callback (only its address is observed).
struct ScNotify : public SmNotifyCallback
{
SmStatus Execute(SmNotifyOperation, SmObject*, SmObject*, SmObject*, SmObject*) override
{
return 0;
}
};
// Sets the system tolerance statics, restoring the defaults on scope exit.
struct ScSystemTol
{
ScSystemTol(double dModelSize, double dRatio)
{
SmTol::SetModelSizeEstimate(dModelSize);
SmTol::SetLargeSmallSizeRatio(dRatio);
}
~ScSystemTol()
{
SmTol::SetModelSizeEstimate(SM_USE_DEFAULT);
SmTol::SetLargeSmallSizeRatio(SM_USE_DEFAULT);
}
};
enum
{
OP_CUR_MARK = 0,
OP_IS_LOCKED = 1,
OP_LOCK_NEW = 2,
OP_LOCK_PAIR = 3,
OP_LOCK_DEFAULT = 4,
OP_LOCK_NEWMARK = 5,
OP_LOCK_SETCTX = 6,
OP_LOCK_SETCTX_PAIR = 7,
OP_LOCK_DROP = 8,
OP_LOCK_INFO = 9,
OP_DOING_BOOL = 10,
OP_GET_MODEL = 11,
OP_SET_MODEL = 12,
OP_SET_RATIO = 13,
OP_TOL_CTX = 14,
OP_CALC_EDGE = 15,
OP_CALC_VERTEX = 16,
OP_CALLBACK = 17,
OP_MASS = 18,
OP_API = 19,
};
const int64_t kMarkTypes[7] = {SM_MT_NOMARK, SM_MT_MARK, SM_MT_MARK2, SM_MT_MARK3,
SM_MT_MARKIO, SM_MT_MARKASSERT, SM_MT_ALLMARKS};
bool valid_mark(int64_t m)
{
for (int64_t v : kMarkTypes)
if (v == m)
return true;
return false;
}
int64_t f_bits(double d)
{
int64_t i;
static_assert(sizeof(i) == sizeof(d), "f64 size");
memcpy(&i, &d, sizeof(d));
return i;
}
} // namespace
// Runs a script of SmContext / SmNewMarkAndLock operations.
// sys_model, sys_ratio : system SmTol::SetModelSizeEstimate / SetLargeSmallSizeRatio
// ctx_params[2*n_ctx] : (model size, ratio) of each SmContext constructed
// ops[6*n_ops] : per op (op, slot, ctx, ctx2, m, flag); ctx -1 = NULL where allowed
// xs[4*n_ops] : per op float operands
// n_slots : number of SmNewMarkAndLock slots
// Output: tagged (tag, payload) int64 pairs; tag 0 = integer, tag 1 = double bits.
// Per op: its results, then a snapshot of every context. At the end the live locks
// are destroyed in slot order and a final snapshot is written.
SC_EXPORT int32_t sc_geom_context_script(double sys_model, double sys_ratio, const double* ctx_params,
int64_t n_ctx, const int64_t* ops, const double* xs,
int64_t n_ops, int64_t n_slots, int64_t* out, int64_t cap,
int64_t* out_len)
{
if (n_ctx < 1 || n_ctx > 8 || n_ops < 0 || n_slots < 0 || n_slots > 16)
return SC_ERR_ARG;
// validate everything before touching upstream
for (int64_t i = 0; i < n_ops; ++i)
{
const int64_t* o = ops + 6 * i;
int64_t op = o[0], s = o[1], c = o[2], c2 = o[3], m = o[4], fl = o[5];
bool need_slot = op >= OP_LOCK_NEW && op <= OP_LOCK_INFO;
bool null_ok = op == OP_TOL_CTX || op == OP_CALC_EDGE || op == OP_CALC_VERTEX;
bool no_ctx = op == OP_LOCK_DEFAULT || op == OP_LOCK_NEWMARK || op == OP_LOCK_DROP ||
op == OP_LOCK_INFO || op == OP_MASS || op == OP_API;
if (op < 0 || op > OP_API)
return SC_ERR_ARG;
if (need_slot && (s < 0 || s >= n_slots))
return SC_ERR_ARG;
if (!no_ctx && !(c >= 0 && c < n_ctx) && !(null_ok && c == -1))
return SC_ERR_ARG;
if ((op == OP_LOCK_PAIR || op == OP_LOCK_SETCTX_PAIR) && (c2 < 0 || c2 >= n_ctx))
return SC_ERR_ARG;
if ((op == OP_CUR_MARK || op == OP_IS_LOCKED || op == OP_LOCK_SETCTX) && !valid_mark(m))
return SC_ERR_ARG;
if (op == OP_DOING_BOOL && (m < 0 || m > 1))
return SC_ERR_ARG;
if (op == OP_TOL_CTX && (m < 0 || m > 4))
return SC_ERR_ARG;
if (op == OP_MASS && (m < 0 || fl < 0))
return SC_ERR_ARG;
}
SC_TRY
ScSystemTol sSys(sys_model, sys_ratio);
static ScNotify s_Notify;
std::vector<ScCtx*> vCtx;
for (int64_t i = 0; i < n_ctx; ++i)
vCtx.push_back(new ScCtx(ctx_params[2 * i], ctx_params[2 * i + 1]));
std::vector<SmNewMarkAndLock*> vSlots((size_t)n_slots, nullptr);
std::vector<int64_t> res;
auto put_i = [&](int64_t v) {
res.push_back(0);
res.push_back(v);
};
auto put_f = [&](double v) {
res.push_back(1);
res.push_back(f_bits(v));
};
auto ctx_index = [&](SmContext* p) -> int64_t {
for (int64_t i = 0; i < n_ctx; ++i)
if ((SmContext*)vCtx[(size_t)i] == p)
return i;
return -1;
};
auto snapshot = [&]() {
for (ScCtx* p : vCtx)
{
for (int64_t m : kMarkTypes)
{
put_i((int64_t)p->GetCurrentMark((SmMarkType)m));
put_i(p->IsMarkLocked((SmMarkType)m) ? 1 : 0);
}
put_i(p->GetDoingBoolean() ? 1 : 0);
put_i(p->GetSysNotifyCallback() ? 1 : 0);
put_i(p->GetUserNotifyCallback() ? 1 : 0);
put_f(p->RawModel());
put_f(p->RawRatio());
put_f(p->RawZone());
}
};
auto drop_slot = [&](int64_t s) {
delete vSlots[(size_t)s];
vSlots[(size_t)s] = nullptr;
};
for (int64_t i = 0; i < n_ops; ++i)
{
const int64_t* o = ops + 6 * i;
const double* x = xs + 4 * i;
int64_t op = o[0], s = o[1], fl = o[5];
SmMarkType eM = (SmMarkType)o[4];
ScCtx* pC = o[2] >= 0 && o[2] < n_ctx ? vCtx[(size_t)o[2]] : nullptr;
ScCtx* pC2 = o[3] >= 0 && o[3] < n_ctx ? vCtx[(size_t)o[3]] : nullptr;
switch (op)
{
case OP_CUR_MARK: put_i((int64_t)pC->GetCurrentMark(eM)); break;
case OP_IS_LOCKED: put_i(pC->IsMarkLocked(eM) ? 1 : 0); break;
case OP_LOCK_NEW:
drop_slot(s);
vSlots[(size_t)s] = new SmNewMarkAndLock(pC, (SmBitArray)o[4]);
put_i(vSlots[(size_t)s]->GetMarkType());
break;
case OP_LOCK_PAIR:
drop_slot(s);
vSlots[(size_t)s] = new SmNewMarkAndLock((SmContext*)pC, (SmContext*)pC2);
put_i(vSlots[(size_t)s]->GetMarkType1());
put_i(vSlots[(size_t)s]->GetMarkType2());
break;
case OP_LOCK_DEFAULT:
drop_slot(s);
vSlots[(size_t)s] = new SmNewMarkAndLock();
break;
case OP_LOCK_NEWMARK:
if (vSlots[(size_t)s] && vSlots[(size_t)s]->GetContext())
{
vSlots[(size_t)s]->NewMark();
put_i(1);
}
else
put_i(-1);
break;
case OP_LOCK_SETCTX:
if (!vSlots[(size_t)s])
vSlots[(size_t)s] = new SmNewMarkAndLock();
put_i(vSlots[(size_t)s]->SetContext(pC, eM));
break;
case OP_LOCK_SETCTX_PAIR:
if (!vSlots[(size_t)s])
vSlots[(size_t)s] = new SmNewMarkAndLock();
put_i(vSlots[(size_t)s]->SetContext((SmContext*)pC, (SmContext*)pC2));
break;
case OP_LOCK_DROP: drop_slot(s); break;
case OP_LOCK_INFO:
if (!vSlots[(size_t)s])
{
put_i(-2);
break;
}
put_i(vSlots[(size_t)s]->GetMarkType());
put_i(vSlots[(size_t)s]->GetMarkType1());
put_i(vSlots[(size_t)s]->GetMarkType2());
put_i(ctx_index(vSlots[(size_t)s]->GetContext()));
put_i(ctx_index(vSlots[(size_t)s]->GetContext2()));
break;
case OP_DOING_BOOL:
if (fl)
pC->GetDoingBooleanRef() = (SmBoolean)o[4];
else
pC->SetDoingBoolean((SmBoolean)o[4]);
put_i(pC->GetDoingBoolean() ? 1 : 0);
break;
case OP_GET_MODEL:
put_f(pC->GetThisModelSizeEstimate());
put_f(SmTol::GetModelSizeEstimate(*(const SmContext*)pC));
break;
case OP_SET_MODEL:
if (fl)
put_f(SmTol::SetModelSizeEstimate(*(SmContext*)pC, x[0]));
else
put_f(pC->SetThisModelSizeEstimate(x[0]));
break;
case OP_SET_RATIO: pC->SetThisLargeSmallSizeRatio(x[0]); break;
case OP_TOL_CTX:
{
const SmContext* cp = pC;
SmSmallTopoType eS = (SmSmallTopoType)o[4];
put_f(SmTol::GetZoneTol3d(cp, eS));
put_f(SmTol::GetApproxTol3d(cp, eS));
put_f(SmTol::GetXSectTol3d(cp, eS));
put_f(SmTol::CalcFaceZoneTol3d(cp));
break;
}
case OP_CALC_EDGE:
{
SmZoneTol3d sMax(x[2]);
put_f(SmTol::CalcEdgeZoneTol3d(x[0], x[1], fl ? &sMax : (SmZoneTol3d*)NULL,
(const SmContext*)pC));
break;
}
case OP_CALC_VERTEX:
{
SmZoneTol3d sMax(x[2]);
put_f(SmTol::CalcVertexZoneTol3d(x[0], x[1], fl ? &sMax : (SmZoneTol3d*)NULL,
(const SmContext*)pC));
break;
}
case OP_CALLBACK:
if (fl)
pC->SetUserNotifyCallback(o[4] ? &s_Notify : NULL);
else
pC->SetSysNotifyCallback(o[4] ? &s_Notify : NULL);
put_i(pC->GetSysNotifyCallback() ? 1 : 0);
put_i(pC->GetUserNotifyCallback() ? 1 : 0);
break;
case OP_MASS:
{
SmMassPropertyBehavior sDef;
SmMassPropertyBehavior sArg(x[0], x[1], x[2], x[3], (ULONG)o[4], (ULONG)fl);
const SmMassPropertyBehavior& r = s ? sDef : sArg;
put_f(r.m_dDensity);
put_f(r.m_dFaceThickness);
put_f(r.m_dEdgeCrossSectionRadius);
put_f(r.m_dVertexSphereRadius);
put_i((int64_t)r.m_lFaceAreaBehavior);
put_i((int64_t)r.m_lFaceVolumeBehavior);
break;
}
case OP_API:
{
SmApiCreateContext();
SmContext* p = SmApiGetOrCreateContext();
put_i(p != NULL ? 1 : 0);
put_i(p == SmApiGetOrCreateContext() ? 1 : 0);
break;
}
default: break;
}
snapshot();
}
for (int64_t s = 0; s < n_slots; ++s)
drop_slot(s);
snapshot();
for (ScCtx* p : vCtx)
{
p->SetSysNotifyCallback(NULL);
delete p;
}
return sc_copy_out(res.data(), (int64_t)res.size(), out, cap, out_len);
SC_CATCH
}
+29 -2
View File
@@ -198,8 +198,8 @@ agents:
- **System statics live in the `Kernel`.** `s_ZoneTol3d`, `s_TolGapGain`,
`s_ModelSizeEstimate`, `s_LargeSmallSizeRatio` are `kernel.tolerances.{zone_tol_3d,
tol_gap_gain, model_size_estimate, large_small_size_ratio}`. The kernel also stands in
for `SmContext`, so the `context_*` functions are the null-context overloads; a context
with its own `m_sThisZoneTol3d` is `UNPORTED(geom.context)`. The difftest shim sets the
for `SmContext`; since `geom.context` the `context_*` / `calc_*_zone_tol_3d`
functions take `Option<&Context>` (`None` = null pointer, `Some(&k.context)` usual). The difftest shim sets the
statics with `SmTol::SetModelSizeEstimate` / `SetLargeSmallSizeRatio` per case and
restores the defaults afterwards (the C++ statics are thread-local globals).
- **Overloads needing later types are not in Rust yet** (markers at the top of
@@ -218,3 +218,30 @@ agents:
- **Co-ports.** `sm_sqrt` and `SmVector3d::GetMaxDimension` (unit `geom.vector3d`) are
private helpers marked `// CO-PORT(geom.vector3d)`. The `SM_IS_ZERO_SQUARED` co-port in
`geom/vector2d.rs` now calls `geom::tol` (vector2d difftest re-run: green, 4060 cases).
## geom.context
- **Two-context `SmNewMarkAndLock` constructor bug (ported as is).** In
`SmNewMarkAndLock(SmContext* p1, SmContext* p2)` with `p1 != p2`, upstream picks
`m_eMarkType2 = GetUnlockedMark(*p2)` but then calls `m_pContext1->NewMark(m_eMarkType1)`
(advancing context 1's mark a second time, not context 2's) and
`LockMark(m_pContext2, m_eMarkType1)` (locking mark type 1 in context 2, not type 2).
The destructor then unlocks `m_eMarkType2` in context 2, so when the types differ a
stray lock of type 1 stays on context 2. `geom::context::MarkLock::new_pair` reproduces
this exactly (difftest case `boolean_pair_two_contexts`). `SetContext(p1, p2)` is
correct (locks type 2 in context 2) and does not advance marks.
- **Release build ignores locks.** `NewMark`'s lock checks and `LockMark` /
`UnlockMark`'s state checks are `SM_ASSERT*` (messages only), so locked marks can still
be advanced and double locks are silent; the port drops the assertions.
- **`SmNewMarkAndLock` binds contexts by index.** Rust `MarkLock` stores indices into a
caller-owned `&mut [Context]` instead of pointers and its destructor is the explicit
`release`; `with_mark_lock(&mut Kernel, ..)` is the scoped form for the kernel's own
context (`Kernel::context`). Topology porters: use `k.context` for
`GetContext()->GetCurrentMark(..)` and `with_mark_lock` for `SmNewMarkAndLock`.
- **Build flags.** The reference build has neither `SM_USE_GLOBAL_CACHE` nor
`SM_USE_NEWTOL`, so the constructor is the two-double one and
`SetThisModelSizeEstimate` is the inline header version
(`m_sThisZoneTol3d = SizeEstimateToZoneTol3d(estimate)`, which maps `SM_USE_DEFAULT` to
`SM_MODEL_SIZE_ESTIMATE`, not to the system statics).
- **`SmApiGetOrCreateContext` global.** The SM_API keeps one process-global context;
the port takes the `Option<Context>` slot from the caller.
File diff suppressed because it is too large Load Diff