From 8ba59701b4802e00ccb72a0baf1743f115c79026 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:23:12 -0400 Subject: [PATCH] geometry: tests open a closed hollow box, a post on its void floor and an open tray (#364) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- crates/geometry/tests/designs/README.md | 3 + .../tests/designs/hollow-box-post.json | 64 +++++++++++ .../tests/designs/hollow-box-post.solidcraft | 102 ++++++++++++++++++ crates/geometry/tests/designs/hollow-box.json | 48 +++++++++ .../tests/designs/hollow-box.solidcraft | 76 +++++++++++++ .../tests/designs/open-tray-post.json | 47 ++++++++ .../tests/designs/open-tray-post.solidcraft | 79 ++++++++++++++ crates/geometry/tests/void_shells.rs | 69 ++++++++++++ 8 files changed, 488 insertions(+) create mode 100644 crates/geometry/tests/designs/README.md create mode 100644 crates/geometry/tests/designs/hollow-box-post.json create mode 100644 crates/geometry/tests/designs/hollow-box-post.solidcraft create mode 100644 crates/geometry/tests/designs/hollow-box.json create mode 100644 crates/geometry/tests/designs/hollow-box.solidcraft create mode 100644 crates/geometry/tests/designs/open-tray-post.json create mode 100644 crates/geometry/tests/designs/open-tray-post.solidcraft create mode 100644 crates/geometry/tests/void_shells.rs diff --git a/crates/geometry/tests/designs/README.md b/crates/geometry/tests/designs/README.md new file mode 100644 index 0000000..d73f8f7 --- /dev/null +++ b/crates/geometry/tests/designs/README.md @@ -0,0 +1,3 @@ +Designs saved by SolidCraft, opened by `tests/void_shells.rs` as a person's file would be. Each +`.solidcraft` is `solidcraft-cli run .json --out .solidcraft`; the volumes the +tests expect are analytic. diff --git a/crates/geometry/tests/designs/hollow-box-post.json b/crates/geometry/tests/designs/hollow-box-post.json new file mode 100644 index 0000000..ddb71d2 --- /dev/null +++ b/crates/geometry/tests/designs/hollow-box-post.json @@ -0,0 +1,64 @@ +[ + { + "command": "solid.box", + "params": { + "length": 40, + "width": 30, + "height": 20, + "corner": [ + 0, + 0, + 0 + ], + "body_name": "Case" + } + }, + { + "command": "solid.shell", + "params": { + "faces": [ + [ + 20, + 15, + 20 + ] + ], + "thickness": 2, + "direction": "inside", + "body": "Case" + } + }, + { + "command": "solid.box", + "params": { + "length": 40, + "width": 30, + "height": 2, + "corner": [ + 0, + 0, + 20 + ], + "operation": "join", + "targets": [ + "Case" + ] + } + }, + { + "command": "solid.cylinder", + "params": { + "diameter": 6, + "height": 5, + "base": [ + 8, + 8, + 2 + ], + "operation": "join", + "targets": [ + "Case" + ] + } + } +] \ No newline at end of file diff --git a/crates/geometry/tests/designs/hollow-box-post.solidcraft b/crates/geometry/tests/designs/hollow-box-post.solidcraft new file mode 100644 index 0000000..cd66a6c --- /dev/null +++ b/crates/geometry/tests/designs/hollow-box-post.solidcraft @@ -0,0 +1,102 @@ +{ + "format": "solidcraft/21", + "name": "Untitled", + "units": "mm", + "params": [], + "features": [ + { + "id": 1, + "name": "Box1", + "suppressed": false, + "body_names": [ + "Case" + ], + "param_names": [ + "d1", + "d2", + "d3" + ], + "type": "box", + "corner": [ + 0.0, + 0.0, + 0.0 + ], + "length": "40", + "width": "30", + "height": "20", + "operation": "new_body" + }, + { + "id": 2, + "name": "Shell1", + "suppressed": false, + "param_names": [ + "d4" + ], + "face_names": [ + "F1:+z" + ], + "type": "shell", + "faces": [ + [ + 20.0, + 15.0, + 20.0 + ] + ], + "thickness": "2", + "body": "Case" + }, + { + "id": 3, + "name": "Box2", + "suppressed": false, + "param_names": [ + "d5", + "d6", + "d7" + ], + "participants": [ + "Case" + ], + "type": "box", + "corner": [ + 0.0, + 0.0, + 20.0 + ], + "length": "40", + "width": "30", + "height": "2", + "operation": "join" + }, + { + "id": 4, + "name": "Cylinder1", + "suppressed": false, + "param_names": [ + "d8", + "d9" + ], + "participants": [ + "Case" + ], + "type": "cylinder", + "base": [ + 8.0, + 8.0, + 2.0 + ], + "axis": [ + 0.0, + 0.0, + 1.0 + ], + "radius": "(6 mm) / 2", + "height": "5", + "operation": "join" + } + ], + "next_id": 5 +} \ No newline at end of file diff --git a/crates/geometry/tests/designs/hollow-box.json b/crates/geometry/tests/designs/hollow-box.json new file mode 100644 index 0000000..e931685 --- /dev/null +++ b/crates/geometry/tests/designs/hollow-box.json @@ -0,0 +1,48 @@ +[ + { + "command": "solid.box", + "params": { + "length": 40, + "width": 30, + "height": 20, + "corner": [ + 0, + 0, + 0 + ], + "body_name": "Case" + } + }, + { + "command": "solid.shell", + "params": { + "faces": [ + [ + 20, + 15, + 20 + ] + ], + "thickness": 2, + "direction": "inside", + "body": "Case" + } + }, + { + "command": "solid.box", + "params": { + "length": 40, + "width": 30, + "height": 2, + "corner": [ + 0, + 0, + 20 + ], + "operation": "join", + "targets": [ + "Case" + ] + } + } +] \ No newline at end of file diff --git a/crates/geometry/tests/designs/hollow-box.solidcraft b/crates/geometry/tests/designs/hollow-box.solidcraft new file mode 100644 index 0000000..9e6f4eb --- /dev/null +++ b/crates/geometry/tests/designs/hollow-box.solidcraft @@ -0,0 +1,76 @@ +{ + "format": "solidcraft/21", + "name": "Untitled", + "units": "mm", + "params": [], + "features": [ + { + "id": 1, + "name": "Box1", + "suppressed": false, + "body_names": [ + "Case" + ], + "param_names": [ + "d1", + "d2", + "d3" + ], + "type": "box", + "corner": [ + 0.0, + 0.0, + 0.0 + ], + "length": "40", + "width": "30", + "height": "20", + "operation": "new_body" + }, + { + "id": 2, + "name": "Shell1", + "suppressed": false, + "param_names": [ + "d4" + ], + "face_names": [ + "F1:+z" + ], + "type": "shell", + "faces": [ + [ + 20.0, + 15.0, + 20.0 + ] + ], + "thickness": "2", + "body": "Case" + }, + { + "id": 3, + "name": "Box2", + "suppressed": false, + "param_names": [ + "d5", + "d6", + "d7" + ], + "participants": [ + "Case" + ], + "type": "box", + "corner": [ + 0.0, + 0.0, + 20.0 + ], + "length": "40", + "width": "30", + "height": "2", + "operation": "join" + } + ], + "next_id": 4 +} \ No newline at end of file diff --git a/crates/geometry/tests/designs/open-tray-post.json b/crates/geometry/tests/designs/open-tray-post.json new file mode 100644 index 0000000..4c32e3e --- /dev/null +++ b/crates/geometry/tests/designs/open-tray-post.json @@ -0,0 +1,47 @@ +[ + { + "command": "solid.box", + "params": { + "length": 40, + "width": 30, + "height": 20, + "corner": [ + 0, + 0, + 0 + ], + "body_name": "Case" + } + }, + { + "command": "solid.shell", + "params": { + "faces": [ + [ + 20, + 15, + 20 + ] + ], + "thickness": 2, + "direction": "inside", + "body": "Case" + } + }, + { + "command": "solid.cylinder", + "params": { + "diameter": 6, + "height": 5, + "base": [ + 8, + 8, + 2 + ], + "operation": "join", + "targets": [ + "Case" + ] + } + } +] \ No newline at end of file diff --git a/crates/geometry/tests/designs/open-tray-post.solidcraft b/crates/geometry/tests/designs/open-tray-post.solidcraft new file mode 100644 index 0000000..9bba2a0 --- /dev/null +++ b/crates/geometry/tests/designs/open-tray-post.solidcraft @@ -0,0 +1,79 @@ +{ + "format": "solidcraft/21", + "name": "Untitled", + "units": "mm", + "params": [], + "features": [ + { + "id": 1, + "name": "Box1", + "suppressed": false, + "body_names": [ + "Case" + ], + "param_names": [ + "d1", + "d2", + "d3" + ], + "type": "box", + "corner": [ + 0.0, + 0.0, + 0.0 + ], + "length": "40", + "width": "30", + "height": "20", + "operation": "new_body" + }, + { + "id": 2, + "name": "Shell1", + "suppressed": false, + "param_names": [ + "d4" + ], + "face_names": [ + "F1:+z" + ], + "type": "shell", + "faces": [ + [ + 20.0, + 15.0, + 20.0 + ] + ], + "thickness": "2", + "body": "Case" + }, + { + "id": 3, + "name": "Cylinder1", + "suppressed": false, + "param_names": [ + "d5", + "d6" + ], + "participants": [ + "Case" + ], + "type": "cylinder", + "base": [ + 8.0, + 8.0, + 2.0 + ], + "axis": [ + 0.0, + 0.0, + 1.0 + ], + "radius": "(6 mm) / 2", + "height": "5", + "operation": "join" + } + ], + "next_id": 4 +} \ No newline at end of file diff --git a/crates/geometry/tests/void_shells.rs b/crates/geometry/tests/void_shells.rs new file mode 100644 index 0000000..a1e8018 --- /dev/null +++ b/crates/geometry/tests/void_shells.rs @@ -0,0 +1,69 @@ +//! A solid with a void shell (a box shelled, then capped) opens with every shell and everything +//! joined to them (#364); a feature that fails to rebuild is reported naming the body it changes. + +use fieldcraft_geometry::{Design, open_design}; + +const PI: f64 = std::f64::consts::PI; +/// The outer box, 40 × 30 × 20 mm, and its cavity with 2 mm walls and floor. +const BOX: f64 = 40.0 * 30.0 * 20.0; +const CAVITY: f64 = 36.0 * 26.0 * 18.0; +const LID: f64 = 40.0 * 30.0 * 2.0; +/// A post ⌀6 × 5 mm standing on the floor. +const POST: f64 = PI * 9.0 * 5.0; + +fn open(bytes: &[u8]) -> Design { + let design = open_design(bytes).unwrap(); + assert!(design.errors.is_empty(), "{:?}", design.errors); + assert_eq!(design.bodies.len(), 1); + design +} + +fn check(design: &Design, volume: f64, faces: usize) { + let body = design.body("Case").unwrap(); + let m = body.measure().unwrap(); + assert!((m.volume - volume).abs() < 1e-5 * volume, "{} vs {volume}", m.volume); + assert_eq!(m.faces, faces); + let mut names: Vec<&str> = body.faces().iter().map(|f| f.name.as_str()).collect(); + assert_eq!(names.len(), faces); + names.sort_unstable(); + names.dedup(); + assert_eq!(names.len(), faces, "every face has its own name"); + // The mesh sees the same solid: every face tessellated, the same enclosed volume. + let mesh = body.tessellate(0.01).unwrap(); + let mut tags = mesh.tri_face.clone(); + tags.sort_unstable(); + tags.dedup(); + assert_eq!(tags.len(), faces); + assert!((mesh.enclosed_volume() - volume).abs() < 1e-3 * volume, "{} vs {volume}", mesh.enclosed_volume()); +} + +#[test] +fn a_closed_hollow_box_keeps_its_void_shell() { + // Six outside faces and six cavity faces. + check(&open(include_bytes!("designs/hollow-box.solidcraft")), BOX - CAVITY + LID, 12); +} + +#[test] +fn a_post_joined_to_the_void_floor_comes_through() { + // The hollow box's twelve faces, the post's top and its side (two halves). + check(&open(include_bytes!("designs/hollow-box-post.solidcraft")), BOX - CAVITY + LID + POST, 15); +} + +#[test] +fn a_post_in_an_open_tray_comes_through() { + // Five outside faces, the rim, five cavity faces and the post's three. + check(&open(include_bytes!("designs/open-tray-post.solidcraft")), BOX - CAVITY + POST, 14); +} + +#[test] +fn a_feature_that_fails_names_the_body_it_changes() { + let text = std::str::from_utf8(include_bytes!("designs/hollow-box-post.solidcraft")).unwrap(); + assert!(text.contains(r#""height": "5""#)); + let broken = text.replace(r#""height": "5""#, r#""height": "0""#); + let design = open_design(broken.as_bytes()).unwrap(); + let [error] = design.errors.as_slice() else { panic!("{:?}", design.errors) }; + assert_eq!(error.feature, "Cylinder1"); + assert_eq!(error.bodies, ["Case"]); + let warning = error.warning(); + assert!(warning.starts_with(r#"Cylinder1 failed to build, so body "Case" may differ from the design: "#), "{warning}"); +}