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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 09:23:40 -04:00
1 parent 358ed2f54f
commit 8ba59701b4
8 files changed
+488

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
Designs saved by SolidCraft, opened by `tests/void_shells.rs` as a person's file would be. Each
`<name>.solidcraft` is `solidcraft-cli run <name>.json --out <name>.solidcraft`; the volumes the
tests expect are analytic.
@@ -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"
]
}
}
]
@@ -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
}
@@ -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"
]
}
}
]
@@ -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
}
@@ -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"
]
}
}
]
@@ -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
}
+69
View File
@@ -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}");
}