No panics in the boolean's final assembly (the web build aborts on any panic: the Plastic Enclosure sample panicked 8 times and crashed the browser's start page); face pushes check their result and fall back to the general offset (a boss standing on a plate joins again with off-axis cylinder seams); offset errors name the face they can't move; docs/robustness.md with the before and after rates; caught panics counted per design in the robustness runs
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
ccde7a4837
commit
1bf377c14b
8 files changed
+182
-33
No files matched your search
@@ -79,6 +79,37 @@ fn shell_thicker_than_a_step_says_so() {
|
||||
assert!(e.to_string().contains("not supported yet"), "{e}");
|
||||
}
|
||||
|
||||
/// Panics inside the kernel are caught natively, but abort the web build (wasm can't unwind).
|
||||
/// Building the samples must not panic at all (the Plastic Enclosure's bosses used to, 8 times).
|
||||
#[test]
|
||||
fn samples_build_without_panics() {
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
let me = std::thread::current().id();
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let c2 = count.clone();
|
||||
let prev = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
if std::thread::current().id() == me {
|
||||
c2.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
let _ = info;
|
||||
}));
|
||||
let mut failed = Vec::new();
|
||||
for sample in crate::sample::samples() {
|
||||
let mut s = Session::default();
|
||||
for c in (sample.script)()["commands"].as_array().into_iter().flatten() {
|
||||
let _ = s.execute(c["command"].as_str().unwrap_or(""), &c["params"]);
|
||||
}
|
||||
let n = count.swap(0, Ordering::SeqCst);
|
||||
if n > 0 {
|
||||
failed.push(format!("{}: {n} panic(s)", sample.name));
|
||||
}
|
||||
}
|
||||
std::panic::set_hook(prev);
|
||||
assert!(failed.is_empty(), "{failed:?}");
|
||||
}
|
||||
|
||||
/// Seeds that run clean: no failure, no invalid body, no volume going the wrong way.
|
||||
#[test]
|
||||
fn fixed_seeds_run_clean() {
|
||||
|
||||
@@ -42,8 +42,11 @@ fn planes(b: &Body) -> Vec<(usize, PlaneFace)> {
|
||||
|
||||
/// Push the planar faces of `b` lying in the plane (n, d) — n their outward normal — along n
|
||||
/// by `delta` (negative: inward). Their neighbours must stand perpendicular to the plane.
|
||||
fn push_faces(b: &Body, n: Vec3, d: f64, delta: f64) -> Result<Body> {
|
||||
match push_faces_exact(b, n, d, delta) {
|
||||
pub(crate) fn push_faces(b: &Body, n: Vec3, d: f64, delta: f64) -> Result<Body> {
|
||||
// The exact rebuild, unless it came out unsound (a curved side it couldn't re-sweep).
|
||||
match push_faces_exact(b, n, d, delta)
|
||||
.and_then(|r| if r.validity().is_empty() { Ok(r) } else { Err(KernelError::Failed("pushing a face: an unsound result".into())) })
|
||||
{
|
||||
Ok(r) => Ok(r),
|
||||
// Faces of planes and cylinders: every face follows (any neighbours).
|
||||
Err(e) => {
|
||||
@@ -66,7 +69,7 @@ fn push_faces(b: &Body, n: Vec3, d: f64, delta: f64) -> Result<Body> {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_faces_exact(b: &Body, n: Vec3, d: f64, delta: f64) -> Result<Body> {
|
||||
pub(crate) fn push_faces_exact(b: &Body, n: Vec3, d: f64, delta: f64) -> Result<Body> {
|
||||
let size = b.size();
|
||||
let tol = (size * 1e-7).max(1e-9);
|
||||
let fail = |m: &str| KernelError::Failed(format!("pushing a face: {m}"));
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Surf {
|
||||
fn grad(&self, p: Vec3) -> Option<Vec3> {
|
||||
match *self {
|
||||
Surf::Plane { n, .. } => Some(n),
|
||||
Surf::Cylinder { convex, .. } | Surf::Sphere { convex, .. } | Surf::Cone { convex, .. } => {
|
||||
Surf::Cylinder { convex, .. } | Surf::Sphere { convex, .. } | Surf::Cone { convex, .. } | Surf::Torus { convex, .. } => {
|
||||
self.normal(p).map(|n| if convex { n } else { -n })
|
||||
}
|
||||
}
|
||||
@@ -454,7 +454,13 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
|
||||
let mut new = Vec::with_capacity(faces.len());
|
||||
let mut shifts = Vec::with_capacity(faces.len());
|
||||
for (i, f) in faces.iter().enumerate() {
|
||||
let sf = surf_of(f, tol * 100.0).ok_or_else(|| fail("only bodies with planes, cylinders, cones, spheres and tori"))?;
|
||||
let sf = surf_of(f, tol * 100.0).ok_or_else(|| {
|
||||
let at = samples(f).first().map(|(p, _)| format!(" (a face at [{:.2}, {:.2}, {:.2}])", p.x, p.y, p.z)).unwrap_or_default();
|
||||
if std::env::var("DBG_OFFSET").is_ok() {
|
||||
eprintln!("unrecognised face: {:?}", f.surface());
|
||||
}
|
||||
fail(&format!("only bodies with planes, cylinders, cones, spheres and tori{at}"))
|
||||
})?;
|
||||
let n = match sf {
|
||||
Surf::Plane { n, .. } => n,
|
||||
Surf::Cylinder { a, .. } | Surf::Cone { a, .. } | Surf::Torus { a, .. } => a.any_perp(),
|
||||
|
||||
@@ -1230,7 +1230,8 @@ mod tests {
|
||||
let mut tops: Vec<f64> = s.faces(0.01).unwrap().iter().filter(|f| (f.centroid.z - 20.0).abs() < 1e-6).map(|f| f.area).collect();
|
||||
tops.sort_by(f64::total_cmp);
|
||||
let pi = std::f64::consts::PI;
|
||||
assert!(rel(tops[0], pi * 16.0) < 1e-3 && rel(tops[1], pi * 84.0) < 1e-3, "{tops:?}");
|
||||
// (Face areas come from meshes: within their sag, which depends on where seams lie.)
|
||||
assert!(rel(tops[0], pi * 16.0) < 2e-3 && rel(tops[1], pi * 84.0) < 2e-3, "{tops:?}");
|
||||
}
|
||||
|
||||
/// A plane through a hole in a plate's top: the top splits in two, each piece holding
|
||||
|
||||
Reference in new issue
Block a user