mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 13:01:52 +00:00
d646586d30
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>
228 lines
7.9 KiB
Rust
228 lines
7.9 KiB
Rust
//! The [`Kernel`] value: tolerances, deterministic RNG and caches, passed explicitly to
|
|
//! operations in place of upstream's process-global context.
|
|
//!
|
|
//! Upstream: `source/SMLib/inc/SmContext.h` (`SmContext`), `SmTol.h` (`SmTol`,
|
|
//! `SmTol3d`), the tolerance defaults of `SmMath.h`, the `SmCacheMgr*` classes, and
|
|
//! `SM_API`'s `SmApiGetOrCreateContext()`.
|
|
//!
|
|
//! | 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` |
|
|
//! | [`Tolerances::ang_tol_deg`] | `SM_ANG_TOL_DEG` |
|
|
//! | [`Tolerances::tol_gap_gain`] | `SM_TOL_GAP_GAIN` |
|
|
//! | [`Tolerances::model_size_estimate`] | `SmContext::m_dThisModelSizeEstimate`, `SM_MODEL_SIZE_ESTIMATE` |
|
|
//! | [`Tolerances::large_small_size_ratio`] | `SM_LARGE_SMALL_SIZE_RATIO` |
|
|
//! | [`Rng`] | C `rand()` / `srand(time(NULL))` (e.g. `SmShell.cpp`) |
|
|
//! | [`Caches`] | `SmCacheMgr`, `SmCacheMgrCrv/Srf/TSrf/Brep` |
|
|
//!
|
|
//! Upstream seeds `rand()` from the clock in a few places; the port replaces that with
|
|
//! [`Rng`], seeded from [`KernelOptions::seed`], so runs are reproducible. Results that
|
|
//! depend on random draws therefore cannot match the C++ bit for bit; difftests of such
|
|
//! 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 {
|
|
/// Local-neighbourhood size: points closer than this are coincident
|
|
/// (`SM_ZONE_TOL_3D` = `1e-5`).
|
|
pub zone_tol_3d: f64,
|
|
/// Default gap / intersection tolerance (`SM_XSECT_TOL_3D` = `2 * zone`).
|
|
pub xsect_tol_3d: f64,
|
|
/// Default approximation tolerance (`SM_APPROX_TOL_3D` = `zone / 2`).
|
|
pub approx_tol_3d: f64,
|
|
/// Maximum angle between parallel vectors, degrees (`SM_ANG_TOL_DEG` = `0.5`).
|
|
pub ang_tol_deg: f64,
|
|
/// Over-size factor applied above an object's max gap (`SM_TOL_GAP_GAIN` = `1.1`).
|
|
pub tol_gap_gain: f64,
|
|
/// Expected model size used to pick tolerances
|
|
/// (`SM_MODEL_SIZE_ESTIMATE` = `50.0`).
|
|
pub model_size_estimate: f64,
|
|
/// Ratio of largest to smallest feature size
|
|
/// (`SM_LARGE_SMALL_SIZE_RATIO` = `1000`).
|
|
pub large_small_size_ratio: f64,
|
|
}
|
|
|
|
impl Default for Tolerances {
|
|
fn default() -> Self {
|
|
let zone = 1.0e-5;
|
|
Self {
|
|
zone_tol_3d: zone,
|
|
xsect_tol_3d: 2.0 * zone,
|
|
approx_tol_3d: zone / 2.0,
|
|
ang_tol_deg: 0.5,
|
|
tol_gap_gain: 1.1,
|
|
model_size_estimate: 50.0,
|
|
large_small_size_ratio: 1000.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Tolerances {
|
|
/// [`Tolerances::ang_tol_deg`] in radians (`SM_ANG_TOL_RAD`).
|
|
#[must_use]
|
|
pub fn ang_tol_rad(&self) -> f64 {
|
|
self.ang_tol_deg * crate::math::PI / 180.0
|
|
}
|
|
}
|
|
|
|
/// Deterministic pseudo-random generator: `SplitMix64` (Steele, Lea & Flood,
|
|
/// "Fast splittable pseudorandom number generators", OOPSLA 2014).
|
|
///
|
|
/// Chosen because it is tiny, has no dependencies, passes `BigCrush`, behaves the same
|
|
/// on every target (including wasm32) and is `Send`. It is not cryptographic.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Rng {
|
|
state: u64,
|
|
}
|
|
|
|
impl Rng {
|
|
/// Default seed used by [`KernelOptions::default`].
|
|
pub const DEFAULT_SEED: u64 = 0x5EED_5EED_5EED_5EED;
|
|
|
|
/// Creates a generator from a seed. Equal seeds give equal sequences.
|
|
#[must_use]
|
|
pub const fn new(seed: u64) -> Self {
|
|
Self { state: seed }
|
|
}
|
|
|
|
/// Next 64 random bits.
|
|
pub fn next_u64(&mut self) -> u64 {
|
|
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
|
let mut z = self.state;
|
|
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
|
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
|
z ^ (z >> 31)
|
|
}
|
|
|
|
/// Uniform `f64` in `[0, 1)` built from the top 53 bits.
|
|
pub fn next_f64(&mut self) -> f64 {
|
|
(self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
|
|
}
|
|
|
|
/// Uniform `f64` in `[-1, 1)`; the port of upstream's
|
|
/// `2.0*((double)rand()/RAND_MAX) - 1.0` idiom.
|
|
pub fn next_signed_unit(&mut self) -> f64 {
|
|
2.0 * self.next_f64() - 1.0
|
|
}
|
|
}
|
|
|
|
/// Memoisation caches owned by a [`Kernel`] (upstream `SmCacheMgr*`).
|
|
///
|
|
/// Empty until the cache units are ported; clearing must never change results, only
|
|
/// speed.
|
|
#[derive(Debug, Default)]
|
|
#[non_exhaustive]
|
|
pub struct Caches {}
|
|
|
|
impl Caches {
|
|
/// Drops all cached data.
|
|
pub fn clear(&mut self) {
|
|
*self = Self::default();
|
|
}
|
|
}
|
|
|
|
/// Options for [`Kernel::new`].
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct KernelOptions {
|
|
/// Tolerances to use.
|
|
pub tolerances: Tolerances,
|
|
/// Seed for the kernel's [`Rng`].
|
|
pub seed: u64,
|
|
}
|
|
|
|
impl Default for KernelOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
tolerances: Tolerances::default(),
|
|
seed: Rng::DEFAULT_SEED,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Per-session kernel state passed to operations (upstream `SmContext`).
|
|
///
|
|
/// A `Kernel` is `Send` (it can move between threads) and holds no global state, so
|
|
/// independent kernels never interfere.
|
|
#[derive(Debug)]
|
|
pub struct Kernel {
|
|
/// Geometric tolerances.
|
|
pub tolerances: Tolerances,
|
|
/// Deterministic random-number generator.
|
|
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 {
|
|
fn default() -> Self {
|
|
Self::new(&KernelOptions::default())
|
|
}
|
|
}
|
|
|
|
impl Kernel {
|
|
/// Creates a kernel from options.
|
|
#[must_use]
|
|
pub fn new(options: &KernelOptions) -> Self {
|
|
Self {
|
|
tolerances: options.tolerances,
|
|
rng: Rng::new(options.seed),
|
|
caches: Caches::default(),
|
|
context: Context::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn kernel_is_send() {
|
|
fn assert_send<T: Send>() {}
|
|
assert_send::<Kernel>();
|
|
}
|
|
|
|
#[test]
|
|
fn default_tolerances_match_smmath() {
|
|
let t = Tolerances::default();
|
|
assert_eq!(t.zone_tol_3d, 1.0e-5);
|
|
assert_eq!(t.xsect_tol_3d, 2.0e-5);
|
|
assert_eq!(t.approx_tol_3d, 5.0e-6);
|
|
assert_eq!(t.ang_tol_rad(), 0.5 * crate::math::PI / 180.0);
|
|
}
|
|
|
|
#[test]
|
|
fn splitmix64_reference_sequence() {
|
|
// Reference values of SplitMix64 seeded with 0 (Vigna's splitmix64.c).
|
|
let mut r = Rng::new(0);
|
|
assert_eq!(r.next_u64(), 0xE220_A839_7B1D_CDAF);
|
|
assert_eq!(r.next_u64(), 0x6E78_9E6A_A1B9_65F4);
|
|
assert_eq!(r.next_u64(), 0x06C4_5D18_8009_454F);
|
|
}
|
|
|
|
#[test]
|
|
fn rng_is_deterministic_and_in_range() {
|
|
let mut k1 = Kernel::default();
|
|
let mut k2 = Kernel::new(&KernelOptions::default());
|
|
for _ in 0..1000 {
|
|
let a = k1.rng.next_f64();
|
|
assert_eq!(a, k2.rng.next_f64());
|
|
assert!((0.0..1.0).contains(&a));
|
|
let s = k1.rng.next_signed_unit();
|
|
assert_eq!(s, k2.rng.next_signed_unit());
|
|
assert!((-1.0..1.0).contains(&s));
|
|
}
|
|
k1.caches.clear();
|
|
}
|
|
}
|