mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 12:31:56 +00:00
dc38349cbf
* Kernel: the B-rep record and the native body file come from solidcore-record SolidCraft's portable B-rep record (`solidcraft_geom::brep`: brep.rs, topology, history, envelope, part, curve2) and the native body file (`solidcraft_kernel::native`: the tagged binary codec and the container) move to solidcore's `solidcore-record` crate: geometry as data, with structural validation and no maths (solidcore's docs/layers.md, stage 1). Every caller uses `solidcore_record::brep` and `solidcore_record::native`; the old copies are deleted. What computes from coordinates stays with the kernel, in geom's `brep`: - `brep::check`: the record's geometric checks (orthonormal frames, non-degenerate planes, unit revolution axes, non-singular placements). `check::validate` and `check::validate_built` are the record's validation and these together, and every caller of `Brep::validate` uses them, so what is accepted is unchanged; - `brep::label`: a surface's natural ranges (a cone's apex), `curve_interval`, `edge_curve_interval` and `form_turns`; - `brep::pcurve::eval`: a pcurve's point; - `geometry::check`, `placement::moved` and `split_edges` become functions on the record. The kernel's native module keeps the conversion between a body and its saved record (metadata.rs) and checks the decoded record's geometry. `native/analytic.rs`, which no module declared, is deleted. solidcore-record is pinned to its rev on solidcore's `layer-record` branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT * Geometry: Vec2, Vec3, Plane and the profile types come from solidcore-record The plain data of vectors and points, sketch planes and planar profiles (`Seg2`, `Loop2`, `Region2`) moves to `solidcore-record`, with its constants and component-wise operators only (owner decision, solidcore layer split). Their geometry stays in geom as traits: `Vec2Geom`, `Vec3Geom`, `PlaneGeom`, `Seg2Geom`, `Loop2Geom`, `Region2Geom`, `Seg2ArcFit`, `Loop2ArcFit` and `Region2ArcFit`, in scope with `use solidcraft_geom::prelude::*`. Every caller names the types from `solidcore_record`; geom no longer defines or re-exports them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT * Kernel: pin solidcore-record to solidcore main f5e8e9a29 solidcore-record is merged on solidcore main (tangency-labs/solidcore#2519); its crate there is identical to the rev pinned before but for its clippy configuration. The lock is regenerated against the merged rev. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
5.9 KiB
Rust
162 lines
5.9 KiB
Rust
//! Robustness checks on random inputs (`solidcraft-cli fuzz`, docs/robustness.md): a small
|
||
//! seeded random generator, and the boolean volume identity on random pairs of primitives
|
||
//! placed on a coarse grid, so coincident faces and tangencies come up often.
|
||
|
||
use serde::Serialize;
|
||
use solidcore_record::Vec3;
|
||
|
||
use crate::{Body, BoolOp, boolean, box_solid, cylinder, measure, sphere, torus};
|
||
|
||
/// xorshift64*: small, fast, and the same everywhere (wasm included).
|
||
#[derive(Clone, Debug)]
|
||
pub struct Rng(u64);
|
||
|
||
impl Rng {
|
||
pub fn new(seed: u64) -> Rng {
|
||
// Spread nearby seeds apart (splitmix64), never zero.
|
||
let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||
Rng((z ^ (z >> 31)) | 1)
|
||
}
|
||
pub fn next_u64(&mut self) -> u64 {
|
||
let mut x = self.0;
|
||
x ^= x >> 12;
|
||
x ^= x << 25;
|
||
x ^= x >> 27;
|
||
self.0 = x;
|
||
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
|
||
}
|
||
/// 0…n−1 (0 when n is 0).
|
||
pub fn below(&mut self, n: usize) -> usize {
|
||
if n == 0 { 0 } else { (self.next_u64() % n as u64) as usize }
|
||
}
|
||
/// In [0, 1).
|
||
pub fn unit(&mut self) -> f64 {
|
||
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
|
||
}
|
||
pub fn chance(&mut self, p: f64) -> bool {
|
||
self.unit() < p
|
||
}
|
||
pub fn pick<T: Copy>(&mut self, xs: &[T]) -> Option<T> {
|
||
xs.get(self.below(xs.len())).copied()
|
||
}
|
||
/// A multiple of `step` in [lo, hi].
|
||
pub fn grid(&mut self, lo: f64, hi: f64, step: f64) -> f64 {
|
||
let n = ((hi - lo) / step).floor().max(0.0) as usize;
|
||
lo + step * self.below(n + 1) as f64
|
||
}
|
||
}
|
||
|
||
/// A primitive used by the boolean checks, as plain data (so a failure can be written down).
|
||
#[derive(Clone, Debug, Serialize)]
|
||
pub enum Shape {
|
||
Box { a: [f64; 3], b: [f64; 3] },
|
||
Cylinder { base: [f64; 3], axis: [f64; 3], radius: f64, height: f64 },
|
||
Sphere { center: [f64; 3], radius: f64 },
|
||
Torus { center: [f64; 3], major: f64, minor: f64 },
|
||
}
|
||
|
||
impl Shape {
|
||
pub fn body(&self) -> crate::Result<Body> {
|
||
let v = |p: &[f64; 3]| Vec3::new(p[0], p[1], p[2]);
|
||
match self {
|
||
Shape::Box { a, b } => box_solid(v(a), v(b)),
|
||
Shape::Cylinder { base, axis, radius, height } => cylinder(v(base), v(axis), *radius, *height),
|
||
Shape::Sphere { center, radius } => sphere(v(center), *radius),
|
||
Shape::Torus { center, major, minor } => torus(v(center), *major, *minor),
|
||
}
|
||
}
|
||
|
||
fn random(r: &mut Rng) -> Shape {
|
||
let g = 2.5;
|
||
let mut p = || [r.grid(0.0, 30.0, g), r.grid(0.0, 30.0, g), r.grid(0.0, 30.0, g)];
|
||
let at = p();
|
||
match r.below(10) {
|
||
0..=4 => {
|
||
let size = [r.grid(5.0, 30.0, g), r.grid(5.0, 30.0, g), r.grid(5.0, 30.0, g)];
|
||
Shape::Box { a: at, b: [at[0] + size[0], at[1] + size[1], at[2] + size[2]] }
|
||
}
|
||
5..=7 => {
|
||
let axis = r.pick(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]).unwrap_or([0.0, 0.0, 1.0]);
|
||
Shape::Cylinder { base: at, axis, radius: r.grid(2.5, 10.0, g), height: r.grid(5.0, 30.0, g) }
|
||
}
|
||
8 => Shape::Sphere { center: at, radius: r.grid(2.5, 12.5, g) },
|
||
_ => {
|
||
let minor = r.grid(2.5, 5.0, g);
|
||
Shape::Torus { center: at, major: minor + r.grid(2.5, 10.0, g), minor }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// One boolean check: the shapes, what went wrong (none when the identity holds).
|
||
#[derive(Clone, Debug, Serialize)]
|
||
pub struct BoolCase {
|
||
pub seed: u64,
|
||
pub a: Shape,
|
||
pub b: Shape,
|
||
/// "ok", or the kind of failure: "error", "invalid", "identity".
|
||
pub outcome: String,
|
||
pub detail: String,
|
||
}
|
||
|
||
fn vol(b: &Option<Body>) -> crate::Result<f64> {
|
||
match b {
|
||
Some(b) => Ok(measure(b)?.volume),
|
||
None => Ok(0.0),
|
||
}
|
||
}
|
||
|
||
/// The two primitives [`boolean_case`] uses for a seed.
|
||
pub fn boolean_shapes(seed: u64) -> (Shape, Shape) {
|
||
let mut r = Rng::new(seed);
|
||
(Shape::random(&mut r), Shape::random(&mut r))
|
||
}
|
||
|
||
/// Union, intersection and cut of two random primitives: each result is a sound solid, and
|
||
/// V(A∪B) + V(A∩B) = V(A) + V(B) and V(A−B) = V(A) − V(A∩B), within 0.2 %.
|
||
pub fn boolean_case(seed: u64) -> BoolCase {
|
||
let (a, b) = boolean_shapes(seed);
|
||
let mut case = BoolCase { seed, a: a.clone(), b: b.clone(), outcome: "ok".into(), detail: String::new() };
|
||
let fail = |c: &mut BoolCase, kind: &str, d: String| {
|
||
c.outcome = kind.into();
|
||
c.detail = d;
|
||
};
|
||
let (Ok(ba), Ok(bb)) = (a.body(), b.body()) else {
|
||
fail(&mut case, "error", "a primitive could not be built".into());
|
||
return case;
|
||
};
|
||
let mut res = Vec::new();
|
||
for op in [BoolOp::Union, BoolOp::Intersect, BoolOp::Cut] {
|
||
let result = boolean(&ba, &bb, op);
|
||
match result {
|
||
Ok(x) => {
|
||
if let Some(body) = &x {
|
||
let bad = body.validity();
|
||
if !bad.is_empty() {
|
||
fail(&mut case, "invalid", format!("{op:?}: {}", bad.join("; ")));
|
||
return case;
|
||
}
|
||
}
|
||
res.push(x);
|
||
}
|
||
Err(e) => {
|
||
fail(&mut case, "error", format!("{op:?}: {e}"));
|
||
return case;
|
||
}
|
||
}
|
||
}
|
||
let v = |b: &Option<Body>| vol(b).unwrap_or(f64::NAN);
|
||
let (va, vb) = (v(&Some(ba)), v(&Some(bb)));
|
||
let [u, i, c] = &res[..] else { return case };
|
||
let (vu, vi, vc) = (v(u), v(i), v(c));
|
||
let tol = 2e-3 * (va + vb);
|
||
let e1 = (vu + vi - va - vb).abs();
|
||
let e2 = (vc - (va - vi)).abs();
|
||
if !(e1 <= tol && e2 <= tol) {
|
||
fail(&mut case, "identity", format!("A {va:.3}, B {vb:.3}, A∪B {vu:.3}, A∩B {vi:.3}, A−B {vc:.3}: off by {e1:.3} and {e2:.3}"));
|
||
}
|
||
case
|
||
}
|