engine tests: probes ask for the exact point class

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NvoxKQ2rEZvcXNQnhE3Sgs
This commit is contained in:
jyviko
2026-10-30 13:39:25 -04:00
committed by jyviko
parent 3b88f1acc5
commit cdff4ac7c6
20 changed files with 291 additions and 149 deletions
+9 -3
View File
@@ -28,7 +28,7 @@ fn check_wall(s: &Session, count: usize) {
let measure = solidcraft_kernel::measure(&wall.body).unwrap();
assert!((measure.volume - 30_000.0).abs() < 1e-6, "{measure:?}");
assert!(measure.bbox.max.dist(Vec3::new(50.0, 30.0, 20.0)) < 1e-9);
assert!(wall.body.contains(Vec3::new(49.0, 15.0, 10.0)));
assert_eq!(wall.body.classify_point(Vec3::new(49.0, 15.0, 10.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
let cap = solidcraft_kernel::measure(&state.body("Cap").unwrap().body).unwrap();
assert!((cap.volume - 6000.0).abs() < 1e-6);
assert!(cap.bbox.max.dist(Vec3::new(40.0, 30.0, 25.0)) < 1e-9);
@@ -82,7 +82,13 @@ fn face_join_maps_the_picked_instance_body_and_point_together() {
let occurrence = run(&mut s, "occurrence.copy", json!({"component":component,"translate":[100,0,0]}))["occurrence"].as_u64().unwrap();
run(&mut s, "component.activate", json!({"component":component,"occurrence":occurrence}));
let world = s.world_state().unwrap();
let body = world.bodies.iter().find(|body| body.body.contains(Vec3::new(139.0, 15.0, 10.0))).unwrap().name.clone();
let body = world
.bodies
.iter()
.find(|body| body.body.classify_point(Vec3::new(139.0, 15.0, 10.0)).unwrap() == solidcraft_kernel::PointClass::Inside)
.unwrap()
.name
.clone();
assert_ne!(body, "Wall", "exercise an instance body name");
let result = run(&mut s, "solid.extrude", json!({"face":[140,15,10],"face_body":body,"distance":10,"operation":"join"}));
let FeatureKind::Extrude { targets, .. } = &s.doc.feature(result["feature"].as_u64().unwrap()).unwrap().kind else { panic!("extrude") };
@@ -90,7 +96,7 @@ fn face_join_maps_the_picked_instance_body_and_point_together() {
let world = s.world_state().unwrap();
assert_eq!(world.bodies.len(), 2);
for point in [Vec3::new(49.0, 15.0, 10.0), Vec3::new(149.0, 15.0, 10.0)] {
assert!(world.bodies.iter().any(|body| body.body.contains(point)), "{point:?}");
assert!(world.bodies.iter().any(|body| body.body.classify_point(point).unwrap() == solidcraft_kernel::PointClass::Inside), "{point:?}");
}
}
+25 -11
View File
@@ -15,10 +15,15 @@ fn check(s: &mut Session, want: f64, hole: bool, height: f64) {
let got: f64 = state.bodies.iter().map(|b| solidcraft_kernel::measure(&b.body).unwrap().volume).sum();
// Legacy linked outlines fit tessellated curves; enforce the existing 1e-4 material envelope.
assert!((got - want).abs() < want * 1e-4, "{got} vs {want}");
assert!(state.bodies.iter().any(|b| b.body.contains(Vec3::new(5.0, 5.0, height - 1.0))));
assert!(!state.bodies.iter().any(|b| b.body.contains(Vec3::new(5.0, 5.0, height + 1.0))));
assert!(state.bodies.iter().any(|b| b.body.classify_point(Vec3::new(5.0, 5.0, height - 1.0)).unwrap() == solidcraft_kernel::PointClass::Inside));
assert!(!state.bodies.iter().any(|b| b.body.classify_point(Vec3::new(5.0, 5.0, height + 1.0)).unwrap() == solidcraft_kernel::PointClass::Inside));
if hole {
assert!(!state.bodies.iter().any(|b| b.body.contains(Vec3::new(20.0, 15.0, height - 1.0))));
assert!(
!state
.bodies
.iter()
.any(|b| b.body.classify_point(Vec3::new(20.0, 15.0, height - 1.0)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
}
}
#[test]
@@ -48,8 +53,12 @@ fn generated_face_profiles_keep_exact_links_live_after_edit_and_reopen() {
let area = 1500.0 - if hole { 25.0 * PI } else { 0.0 };
check(&mut s, area * 35.0, hole, 35.0);
let edited = s.model.state();
assert!(edited.bodies.iter().any(|body| body.body.contains(Vec3::new(49.0, 5.0, 34.0))));
assert!(!edited.bodies.iter().any(|body| body.body.contains(Vec3::new(51.0, 5.0, 34.0))));
assert!(
edited.bodies.iter().any(|body| body.body.classify_point(Vec3::new(49.0, 5.0, 34.0)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
assert!(
!edited.bodies.iter().any(|body| body.body.classify_point(Vec3::new(51.0, 5.0, 34.0)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
assert!(edited.sketch(sketch.0).unwrap().sketch.links.iter().all(|link| link.auto_face && !link.lost));
let solved = edited.sketch(sketch.0).unwrap().plane;
assert!((solved.origin.z - 25.0).abs() < 1e-6);
@@ -116,16 +125,21 @@ fn generated_profiles_and_press_pull_convert_rotated_occurrence_picks_once() {
let world = s.world_state().unwrap();
let occupied = Vec3::new(35.0, 21.0 - height, 15.0);
let absent = Vec3::new(35.0, 19.0 - height, 15.0);
assert!(world.bodies.iter().any(|body| body.body.contains(occupied)));
assert!(!world.bodies.iter().any(|body| body.body.contains(absent)));
assert!(world.bodies.iter().any(|body| body.body.classify_point(occupied).unwrap() == solidcraft_kernel::PointClass::Inside));
assert!(!world.bodies.iter().any(|body| body.body.classify_point(absent).unwrap() == solidcraft_kernel::PointClass::Inside));
if hole {
assert!(!world.bodies.iter().any(|body| body.body.contains(Vec3::new(50.0, 21.0 - height, 25.0))));
assert!(
!world
.bodies
.iter()
.any(|body| body.body.classify_point(Vec3::new(50.0, 21.0 - height, 25.0)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
}
let before_volume: f64 = world.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
let before_bounds: Vec<_> = world.bodies.iter().map(|body| (body.name.clone(), body.mesh().bounds())).collect();
let reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
let after = reopened.world_state().unwrap();
assert!(after.bodies.iter().any(|body| body.body.contains(occupied)));
assert!(after.bodies.iter().any(|body| body.body.classify_point(occupied).unwrap() == solidcraft_kernel::PointClass::Inside));
let v: f64 = after.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
assert!((v - area * height).abs() < area * height * 1e-4);
assert!((v - before_volume).abs() < 1e-6);
@@ -222,8 +236,8 @@ fn repeated_source_face_profile_requires_and_uses_the_exact_active_occurrence()
run(&mut s, "solid.extrude", json!({"face":[55,5,10],"distance":5,"operation":"new"}));
let state = s.world_state().unwrap();
for x in [5.0, 55.0] {
assert!(state.bodies.iter().any(|body| body.body.contains(Vec3::new(x, 5.0, 14.0))));
assert!(!state.bodies.iter().any(|body| body.body.contains(Vec3::new(x, 5.0, 16.0))));
assert!(state.bodies.iter().any(|body| body.body.classify_point(Vec3::new(x, 5.0, 14.0)).unwrap() == solidcraft_kernel::PointClass::Inside));
assert!(!state.bodies.iter().any(|body| body.body.classify_point(Vec3::new(x, 5.0, 16.0)).unwrap() == solidcraft_kernel::PointClass::Inside));
}
}
+8 -2
View File
@@ -100,13 +100,19 @@ fn circular_pattern_about_a_boss_wall_follows_the_boss() {
let holes = |s: &Session, cx: f64, r: f64| {
let st = s.world_state().unwrap();
let body = &st.bodies[0].body;
[(r, 0.0), (0.0, r), (-r, 0.0), (0.0, -r)].iter().all(|(dx, dy)| !body.contains(Vec3::new(cx + dx, 100.0 + dy, 2.5)))
[(r, 0.0), (0.0, r), (-r, 0.0), (0.0, -r)]
.iter()
.all(|(dx, dy)| body.classify_point(Vec3::new(cx + dx, 100.0 + dy, 2.5)).unwrap() == solidcraft_kernel::PointClass::Outside)
};
assert!(holes(&s, 100.0, 30.0), "four holes round the boss");
run(&mut s, "parameters.change", json!({"name": "cx", "expression": "90 mm"}));
assert!(holes(&s, 90.0, 40.0), "the copies turn about the boss where it is now");
let st = s.world_state().unwrap();
assert!(st.bodies[0].body.contains(Vec3::new(100.0, 130.0, 2.5)), "not where the boss was");
assert_eq!(
st.bodies[0].body.classify_point(Vec3::new(100.0, 130.0, 2.5)).unwrap(),
solidcraft_kernel::PointClass::Inside,
"not where the boss was"
);
}
#[test]
+21 -11
View File
@@ -95,8 +95,11 @@ fn rectangular_cut_pattern_counts_only_target_contacts_and_keeps_landing_copies(
assert_eq!(reply["warning"], "2 of 3 cut copies remove no material");
let state = s.world_state().unwrap();
let plate = &state.body("Plate").unwrap().body;
assert!(!plate.contains(solidcraft_geom::Vec3::new(30., 15., 5.)));
assert!(state.body("Neighbor").unwrap().body.contains(solidcraft_geom::Vec3::new(50., 15., 5.)));
assert_eq!(plate.classify_point(solidcraft_geom::Vec3::new(30., 15., 5.)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(
state.body("Neighbor").unwrap().body.classify_point(solidcraft_geom::Vec3::new(50., 15., 5.)).unwrap(),
solidcraft_kernel::PointClass::Inside
);
assert!(rel(volume(&mut s), 24000.0 - 2.0 * PI * 4.0 * 10.0) < 1e-3);
}
@@ -122,8 +125,8 @@ fn feature_rectangular_center_flip_and_oblique_direction_keep_source_and_recompu
for i in [0.0, 1.0, -1.0, 2.0] {
for j in [0.0, 1.0] {
let p = Vec3::new(60.0 + sign * (15.0 * i + 12.0 * j), 60.0 + sign * 16.0 * j, 2.5);
assert!(!body.contains(p), "hole at {p:?}");
assert!(body.contains(p + Vec3::new(0.0, 2.0, 0.0)));
assert_eq!(body.classify_point(p).unwrap(), solidcraft_kernel::PointClass::Outside, "hole at {p:?}");
assert_eq!(body.classify_point(p + Vec3::new(0.0, 2.0, 0.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
}
}
assert!(rel(volume(&mut s), 120.0 * 120.0 * 5.0 - 8.0 * PI * 5.0) < 1e-5);
@@ -159,8 +162,11 @@ fn feature_circular_center_flip_uses_partial_arc_endpoints_and_full_circle_spaci
for slot in [0.0, 1.0, -1.0, 2.0, -2.0, 3.0] {
let a = (step * slot * if flipped { -1.0 } else { 1.0 }).to_radians();
let p = Vec3::new(40.0 + 20.0 * a.cos(), 60.0 + 20.0 * a.sin(), 2.5);
assert!(!st.bodies[0].body.contains(p), "hole at {p:?}");
assert!(st.bodies[0].body.contains(p + Vec3::new(2.0 * a.cos(), 2.0 * a.sin(), 0.0)));
assert_eq!(st.bodies[0].body.classify_point(p).unwrap(), solidcraft_kernel::PointClass::Outside, "hole at {p:?}");
assert_eq!(
st.bodies[0].body.classify_point(p + Vec3::new(2.0 * a.cos(), 2.0 * a.sin(), 0.0)).unwrap(),
solidcraft_kernel::PointClass::Inside
);
}
assert!(rel(volume(&mut s), 120.0 * 120.0 * 5.0 - 6.0 * PI * 5.0) < 1e-5);
let saved = s.doc.to_json();
@@ -761,10 +767,10 @@ fn patterns_and_mirrors_of_a_rib() {
assert_eq!(st.bodies.len(), 1);
let body = &st.bodies[0].body;
for p in inside {
assert!(body.contains(*p), "a rib at {p:?}");
assert_eq!(body.classify_point(*p).unwrap(), solidcraft_kernel::PointClass::Inside, "a rib at {p:?}");
// Beside it, across its thickness: no material (clear of the boss too).
let side = if p.y == 0.0 { Vec3::new(0.0, 3.0, 0.0) } else { Vec3::new(3.0, 0.0, 0.0) };
assert!(!body.contains(*p + side), "nothing beside the rib at {p:?}");
assert_eq!(body.classify_point(*p + side).unwrap(), solidcraft_kernel::PointClass::Outside, "nothing beside the rib at {p:?}");
}
};
check(&s, &ribs);
@@ -857,11 +863,15 @@ fn patterns_and_mirrors_of_patterns_and_mirrors() {
assert_eq!(st.bodies.len(), 1, "{what}");
let body = &st.bodies[0].body;
for [x, y] in &holes {
assert!(!body.contains(Vec3::new(*x, *y, 10.0)), "{what}: a hole at ({x}, {y})");
assert!(body.contains(Vec3::new(*x, *y + 2.5, 10.0)), "{what}: material beside ({x}, {y})");
assert_eq!(body.classify_point(Vec3::new(*x, *y, 10.0)).unwrap(), solidcraft_kernel::PointClass::Outside, "{what}: a hole at ({x}, {y})");
assert_eq!(
body.classify_point(Vec3::new(*x, *y + 2.5, 10.0)).unwrap(),
solidcraft_kernel::PointClass::Inside,
"{what}: material beside ({x}, {y})"
);
}
for [x, y] in &material {
assert!(body.contains(Vec3::new(*x, *y, 10.0)), "{what}: no hole at ({x}, {y})");
assert_eq!(body.classify_point(Vec3::new(*x, *y, 10.0)).unwrap(), solidcraft_kernel::PointClass::Inside, "{what}: no hole at ({x}, {y})");
}
// The listed holes the steps added (in the last two cases `Mir` made the first of them).
let added = holes.len();
@@ -31,10 +31,15 @@ fn check_centered(s: &Session, opposed: bool) {
let volume: f64 = state.bodies.iter().map(|mb| solidcraft_kernel::measure(&mb.body).unwrap().volume).sum();
assert!((volume - expected).abs() < 0.03, "{volume} vs {expected}");
for sign in if opposed { vec![1.0, -1.0] } else { vec![1.0] } {
let body = &state.bodies.iter().find(|mb| mb.body.contains(Vec3::new(sign * 3.0, 5.0, 0.0))).unwrap().body;
assert!(!body.contains(Vec3::new(0.0, 5.0, 3.0)));
assert!(body.contains(Vec3::new(sign * 3.0 * (PI / 6.0).cos(), 5.0, 1.5)));
assert!(body.contains(Vec3::new(sign * 3.0 * (PI / 6.0).cos(), 5.0, -1.5)));
let body = &state
.bodies
.iter()
.find(|mb| mb.body.classify_point(Vec3::new(sign * 3.0, 5.0, 0.0)).unwrap() == solidcraft_kernel::PointClass::Inside)
.unwrap()
.body;
assert_eq!(body.classify_point(Vec3::new(0.0, 5.0, 3.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(Vec3::new(sign * 3.0 * (PI / 6.0).cos(), 5.0, 1.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(sign * 3.0 * (PI / 6.0).cos(), 5.0, -1.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().bounds();
assert!((bounds.min.z + 2.0 * SQRT_2).abs() < 1e-6, "{bounds:?}");
assert!((bounds.max.z - 2.0 * SQRT_2).abs() < 1e-6, "{bounds:?}");
@@ -85,8 +90,8 @@ fn two_sided_revolves_use_the_canonical_negative_and_positive_limits() {
assert!((bounds.max.x - 4.0).abs() < 1e-4);
assert!((bounds.min.z + 2.0).abs() < 1e-6);
assert!((bounds.max.z - 2.0 * 3.0f64.sqrt()).abs() < 1e-6);
assert!(body.contains(Vec3::new(3.0, 5.0, 0.0)));
assert!(!body.contains(Vec3::new(0.0, 5.0, 3.0)));
assert_eq!(body.classify_point(Vec3::new(3.0, 5.0, 0.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(0.0, 5.0, 3.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 30.0 * PI).abs() < 0.03);
}
}
@@ -107,8 +112,8 @@ fn legacy_missing_convention_keeps_geometry_through_edit_redefine_undo_and_reope
assert_eq!(s.doc.find_feature("Turn").unwrap().kind, original);
let state = s.model.state();
let body = &state.bodies[0].body;
assert!(body.contains(Vec3::new(0.0, 5.0, 3.0)));
assert!(!body.contains(Vec3::new(3.0, 5.0, 0.0)));
assert_eq!(body.classify_point(Vec3::new(0.0, 5.0, 3.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(3.0, 5.0, 0.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().bounds();
assert!((bounds.min.x + 2.0 * SQRT_2).abs() < 1e-6);
assert!((bounds.max.x - 2.0 * SQRT_2).abs() < 1e-6);
@@ -118,8 +123,8 @@ fn legacy_missing_convention_keeps_geometry_through_edit_redefine_undo_and_reope
let angle_input = s.doc.find_feature("Turn").unwrap().param_names[0].clone();
run(&mut s, "parameters.change", json!({"name":angle_input,"expression":"60 deg"}));
let changed = s.model.state();
assert!(changed.bodies[0].body.contains(Vec3::new(1.5, 5.0, 3.0 * 3.0f64.sqrt() / 2.0)));
assert!(!changed.bodies[0].body.contains(Vec3::new(3.0, 5.0, 0.0)));
assert_eq!(changed.bodies[0].body.classify_point(Vec3::new(1.5, 5.0, 3.0 * 3.0f64.sqrt() / 2.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(changed.bodies[0].body.classify_point(Vec3::new(3.0, 5.0, 0.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
run(&mut s, "edit.undo", json!({}));
check(&s);
run(&mut s, "timeline.edit", json!({"feature":"Turn","set":{"angle":"90 deg"}}));
@@ -178,8 +183,8 @@ fn symmetric_revolve_preserves_a_translated_rotated_sketch_frame() {
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().bounds();
assert!(bounds.min.dist(Vec3::new(10.0, 20.0 - 2.0 * SQRT_2, 30.0 + SQRT_2)) < 1e-6, "{bounds:?}");
assert!(bounds.max.dist(Vec3::new(20.0, 20.0 + 2.0 * SQRT_2, 34.0)) < 1e-4, "{bounds:?}");
assert!(body.contains(Vec3::new(15.0, 20.0, 33.0)));
assert!(!body.contains(Vec3::new(15.0, 23.0, 30.0)));
assert_eq!(body.classify_point(Vec3::new(15.0, 20.0, 33.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(15.0, 23.0, 30.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 30.0 * PI).abs() < 0.03);
}
@@ -193,7 +198,7 @@ fn copied_revolves_keep_their_convention_and_angular_material() {
run(&mut s, "timeline.paste", json!({"translate":[0,20,0]}));
let state = s.model.state();
let probe = if convention == "sketch_normal" { Vec3::new(3.0, 25.0, 0.0) } else { Vec3::new(0.0, 25.0, 3.0) };
assert!(state.bodies.iter().any(|mb| mb.body.contains(probe)));
assert!(state.bodies.iter().any(|mb| mb.body.classify_point(probe).unwrap() == solidcraft_kernel::PointClass::Inside));
let modes: Vec<_> = s
.doc
.features
@@ -203,7 +208,7 @@ fn copied_revolves_keep_their_convention_and_angular_material() {
assert_eq!(modes.len(), 2);
assert_eq!(modes[0], modes[1]);
let reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
assert!(reopened.model.state().bodies.iter().any(|mb| mb.body.contains(probe)));
assert!(reopened.model.state().bodies.iter().any(|mb| mb.body.classify_point(probe).unwrap() == solidcraft_kernel::PointClass::Inside));
}
}
@@ -222,7 +227,7 @@ fn centered_coplanar_cut_uses_the_actual_angular_interval() {
assert!((total - expected).abs() < 0.05, "{volumes:?}: total{total}, expected{expected}");
assert_eq!(state.bodies.len(), 2);
assert!(volumes.iter().all(|v| (*v - expected / 2.0).abs() < 0.025));
let contains = |p| state.bodies.iter().any(|mb| mb.body.contains(p));
let contains = |p| state.bodies.iter().any(|mb| mb.body.classify_point(p).unwrap() == solidcraft_kernel::PointClass::Inside);
assert!(!contains(Vec3::new(3.0, 5.0, 0.0)));
assert!(contains(Vec3::new(3.0, 5.0, 3.5)));
assert!(contains(Vec3::new(3.0, 5.0, -3.5)));
@@ -238,8 +243,11 @@ fn one_sided_and_full_revolves_keep_the_existing_material() {
turn(&mut s, sign, json!({"angle":angle,"direction":"positive","convention":convention}));
let state = s.model.state();
let body = &state.bodies[0].body;
assert!(body.contains(Vec3::new(3.0 / SQRT_2, 5.0, 3.0 / SQRT_2)));
assert_eq!(body.contains(Vec3::new(-3.0 / SQRT_2, 5.0, -3.0 / SQRT_2)), angle == "360 deg");
assert_eq!(body.classify_point(Vec3::new(3.0 / SQRT_2, 5.0, 3.0 / SQRT_2)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(
body.classify_point(Vec3::new(-3.0 / SQRT_2, 5.0, -3.0 / SQRT_2)).unwrap() == solidcraft_kernel::PointClass::Inside,
angle == "360 deg"
);
let expected = if angle == "360 deg" { 120.0 * PI } else { 30.0 * PI };
assert!((solidcraft_kernel::measure(body).unwrap().volume - expected).abs() < 0.08);
}
@@ -258,10 +266,31 @@ fn point_targets_use_each_profiles_canonical_axis_for_both_axis_signs() {
let expected = 30.0 * PI * if opposed { 2.0 } else { 1.0 };
let volume: f64 = state.bodies.iter().map(|mb| solidcraft_kernel::measure(&mb.body).unwrap().volume).sum();
assert!((volume - expected).abs() < 0.05, "{volume}");
assert!(state.bodies.iter().any(|mb| mb.body.contains(Vec3::new(3.0 / SQRT_2, 5.0, 3.0 / SQRT_2))));
assert!(!state.bodies.iter().any(|mb| mb.body.contains(Vec3::new(3.0 / SQRT_2, 5.0, -3.0 / SQRT_2))));
assert_eq!(state.bodies.iter().any(|mb| mb.body.contains(Vec3::new(-3.0 / SQRT_2, 5.0, 3.0 / SQRT_2))), opposed);
assert!(!state.bodies.iter().any(|mb| mb.body.contains(Vec3::new(-3.0 / SQRT_2, 5.0, -3.0 / SQRT_2))));
assert!(
state
.bodies
.iter()
.any(|mb| mb.body.classify_point(Vec3::new(3.0 / SQRT_2, 5.0, 3.0 / SQRT_2)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
assert!(
!state
.bodies
.iter()
.any(|mb| mb.body.classify_point(Vec3::new(3.0 / SQRT_2, 5.0, -3.0 / SQRT_2)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
assert_eq!(
state
.bodies
.iter()
.any(|mb| mb.body.classify_point(Vec3::new(-3.0 / SQRT_2, 5.0, 3.0 / SQRT_2)).unwrap() == solidcraft_kernel::PointClass::Inside),
opposed
);
assert!(
!state
.bodies
.iter()
.any(|mb| mb.body.classify_point(Vec3::new(-3.0 / SQRT_2, 5.0, -3.0 / SQRT_2)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
}
}
}
@@ -295,9 +324,15 @@ fn curved_targets_keep_only_the_material_before_the_post_for_both_axis_signs() {
assert_eq!(arcs.len(), if opposed { 2 } else { 1 });
for side in if opposed { vec![1.0, -1.0] } else { vec![1.0] } {
let probe = Vec3::new(side * 12.0 * (PI / 6.0).cos(), 6.0, -2.5);
let arc = &arcs.iter().find(|mb| mb.body.contains(probe)).unwrap().body;
assert!(!arc.contains(Vec3::new(-side * 12.0 * (PI / 6.0).cos(), 6.0, -2.5)));
assert!(!arc.contains(Vec3::new(side * 12.0 * (PI / 6.0).cos(), -6.0, -2.5)));
let arc = &arcs.iter().find(|mb| mb.body.classify_point(probe).unwrap() == solidcraft_kernel::PointClass::Inside).unwrap().body;
assert_eq!(
arc.classify_point(Vec3::new(-side * 12.0 * (PI / 6.0).cos(), 6.0, -2.5)).unwrap(),
solidcraft_kernel::PointClass::Outside
);
assert_eq!(
arc.classify_point(Vec3::new(side * 12.0 * (PI / 6.0).cos(), -6.0, -2.5)).unwrap(),
solidcraft_kernel::PointClass::Outside
);
let full = PI * (225.0 - 100.0) * 5.0;
let angle = solidcraft_kernel::measure(arc).unwrap().volume / full * 360.0;
assert!(angle > 60.0 && angle < 90.0, "{angle}");
+5 -1
View File
@@ -1126,7 +1126,11 @@ fn label_rectangle(s: &mut Session) {
}
fn contains(s: &Session, p: [f64; 3]) -> bool {
s.model.state().bodies.first().is_some_and(|b| b.body.contains(solidcraft_geom::Vec3::new(p[0], p[1], p[2])))
s.model
.state()
.bodies
.first()
.is_some_and(|b| b.body.classify_point(solidcraft_geom::Vec3::new(p[0], p[1], p[2])).unwrap() == solidcraft_kernel::PointClass::Inside)
}
/// The LINE entities a DXF draws on `layer` (group codes and values alternate, a line each).
+104 -69
View File
@@ -132,8 +132,8 @@ fn shallow_sheet_pocket_preserves_material_depth_export_undo_and_reopen() {
assert!((volume(&mut s) - 14900.0).abs() < 1e-6);
let state = s.model.state();
let body = &state.bodies[0].body;
assert!(!body.contains(solidcraft_geom::Vec3::new(25.0, 25.0, 0.5)));
assert!(body.contains(solidcraft_geom::Vec3::new(25.0, 25.0, 1.5)));
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(25.0, 25.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(25.0, 25.0, 1.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
}
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!((volume(&mut reopened) - 14900.0).abs() < 1e-6);
@@ -286,10 +286,10 @@ fn intersect_preserves_both_material_regions_of_a_logical_ring_sheet() {
let area: f64 = regions.iter().map(|r| r.area()).sum();
assert!((area - (900.0 - PI * 75.0)).abs() < 1e-8, "area={area}");
let body = &state.bodies[0].body;
assert!(body.contains(solidcraft_geom::Vec3::new(50.0, 30.0, 1.0)));
assert!(!body.contains(solidcraft_geom::Vec3::new(57.0, 30.0, 1.0)));
assert!(body.contains(solidcraft_geom::Vec3::new(64.0, 30.0, 1.0)));
assert!(!body.contains(solidcraft_geom::Vec3::new(70.0, 30.0, 1.0)));
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(50.0, 30.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(57.0, 30.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(64.0, 30.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(70.0, 30.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert!((volume(&mut s) - (900.0 - PI * 75.0) * 2.5).abs() < 0.2);
}
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
@@ -322,8 +322,8 @@ fn machining_a_folded_parallel_panel_preserves_its_pocket_on_unfold() {
run(&mut s, "sheet.unfold", json!({}));
let state = s.model.state();
let body = &state.bodies[0].body;
assert!(!body.contains(Vec3::new(c.x, c.y, 0.5)));
assert!(body.contains(Vec3::new(c.x, c.y, 1.5)));
assert_eq!(body.classify_point(Vec3::new(c.x, c.y, 0.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(Vec3::new(c.x, c.y, 1.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(flat(&mut s)["pockets"][0]["depth_mm"], 1.0);
run(&mut s, "sheet.refold", json!({}));
assert!((volume(&mut s) - (18972.62196081845 - 16.0)).abs() < 0.2);
@@ -342,8 +342,11 @@ fn flipped_sheet_pocket_keeps_depth_in_the_local_thickness_direction() {
}
assert!((volume(&mut s) - 14900.0).abs() < 1e-6);
let state = s.model.state();
assert!(!state.bodies[0].body.contains(solidcraft_geom::Vec3::new(25.0, 25.0, -0.5)));
assert!(state.bodies[0].body.contains(solidcraft_geom::Vec3::new(25.0, 25.0, -1.5)));
assert_eq!(
state.bodies[0].body.classify_point(solidcraft_geom::Vec3::new(25.0, 25.0, -0.5)).unwrap(),
solidcraft_kernel::PointClass::Outside
);
assert_eq!(state.bodies[0].body.classify_point(solidcraft_geom::Vec3::new(25.0, 25.0, -1.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
}
}
@@ -374,10 +377,21 @@ fn multiple_profile_intersect_keeps_tool_union_for_solids_and_sheet_groups() {
}
let state = s.model.state();
for x in [25.0, 65.0] {
assert!(state.bodies.iter().any(|b| b.body.contains(solidcraft_geom::Vec3::new(x, 25.0, 1.0))));
assert!(
state
.bodies
.iter()
.any(|b| b.body.classify_point(solidcraft_geom::Vec3::new(x, 25.0, 1.0)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
}
assert!(!state.bodies.iter().any(|b| b.body.contains(solidcraft_geom::Vec3::new(50.0, 25.0, 1.0))));
assert!(state.bodies.iter().any(|b| b.name == "Control" && b.body.contains(solidcraft_geom::Vec3::new(205.0, 5.0, 5.0))));
assert!(
!state
.bodies
.iter()
.any(|b| b.body.classify_point(solidcraft_geom::Vec3::new(50.0, 25.0, 1.0)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
assert!(state.bodies.iter().any(|b| b.name == "Control"
&& b.body.classify_point(solidcraft_geom::Vec3::new(205.0, 5.0, 5.0)).unwrap() == solidcraft_kernel::PointClass::Inside));
}
}
@@ -417,15 +431,15 @@ fn sheet_pocket_transfer_keeps_local_depth_world_pose_and_history() {
assert_eq!((sh.pockets[0].z0, sh.pockets[0].z1), (0.0, 1.0));
let cut = sh.frame.to_world(solidcraft_geom::Vec2::new(25.0, 25.0)) + sh.frame.normal() * 0.5;
let floor = sh.frame.to_world(solidcraft_geom::Vec2::new(25.0, 25.0)) + sh.frame.normal() * 1.5;
assert!(!mb.body.contains(cut));
assert!(mb.body.contains(floor));
assert_eq!(mb.body.classify_point(cut).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(mb.body.classify_point(floor).unwrap(), solidcraft_kernel::PointClass::Inside);
let world = s.world_state().unwrap();
let body = &world.body(name).unwrap().body;
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap().bounds();
assert!(bounds.min.dist(Vec3::ZERO) < 1e-6, "{bounds:?}");
assert!(bounds.max.dist(Vec3::new(100.0, 60.0, 2.5)) < 1e-6, "{bounds:?}");
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.5)));
assert!(body.contains(Vec3::new(25.0, 25.0, 1.5)));
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 1.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 14900.0).abs() < 1e-6);
}
let mut s = Session::default();
@@ -1234,7 +1248,11 @@ fn cut_through(s: &mut Session, sketch: &str) {
}
fn contains(s: &Session, p: [f64; 3]) -> bool {
s.model.state().bodies.first().is_some_and(|b| b.body.contains(solidcraft_geom::Vec3::new(p[0], p[1], p[2])))
s.model
.state()
.bodies
.first()
.is_some_and(|b| b.body.classify_point(solidcraft_geom::Vec3::new(p[0], p[1], p[2])).unwrap() == solidcraft_kernel::PointClass::Inside)
}
#[test]
@@ -2397,8 +2415,8 @@ fn unrelated_box_cut_preserves_logical_ring_sheet_compound() {
assert_eq!(s.model.state().bodies.len(), 2);
let state = s.model.state();
let body = &state.bodies.iter().find(|b| b.name == sheet_name).unwrap().body;
assert!(body.contains(solidcraft_geom::Vec3::new(50.0, 30.0, 1.25)));
assert!(!body.contains(solidcraft_geom::Vec3::new(57.0, 30.0, 1.25)));
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(50.0, 30.0, 1.25)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(57.0, 30.0, 1.25)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
}
@@ -2433,9 +2451,9 @@ fn specialized_sheet_cuts_respect_component_and_explicit_participants() {
fn ring_cut_preserves_retained_island_through_unfold_and_refold() {
fn material_probes(s: &Session) {
let body = &s.model.state().bodies[0].body;
assert!(body.contains(solidcraft_geom::Vec3::new(50.0, 30.0, 1.25)));
assert!(!body.contains(solidcraft_geom::Vec3::new(57.0, 30.0, 1.25)));
assert!(body.contains(solidcraft_geom::Vec3::new(65.0, 30.0, 1.25)));
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(50.0, 30.0, 1.25)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(57.0, 30.0, 1.25)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(solidcraft_geom::Vec3::new(65.0, 30.0, 1.25)).unwrap(), solidcraft_kernel::PointClass::Inside);
}
let mut s = Session::default();
base(&mut s, 100.0, 60.0);
@@ -2476,10 +2494,14 @@ fn retained_depth_check(s: &mut Session, want: f64, z0: f64, z1: f64) {
let state = s.model.state();
assert_eq!(state.sheets.len(), 1);
assert_eq!(state.sheets[0].t, 2.5, "rule stock gauge is preserved");
assert!(state.bodies.iter().any(|b| b.body.contains(Vec3::new(25.0, 25.0, (z0 + z1) * 0.5))));
assert!(!state.bodies.iter().any(|b| b.body.contains(Vec3::new(25.0, 25.0, z0 - 0.1))));
assert!(!state.bodies.iter().any(|b| b.body.contains(Vec3::new(25.0, 25.0, z1 + 0.1))));
assert!(!state.bodies.iter().any(|b| b.body.contains(Vec3::new(10.0, 25.0, (z0 + z1) * 0.5))));
assert!(
state.bodies.iter().any(|b| b.body.classify_point(Vec3::new(25.0, 25.0, (z0 + z1) * 0.5)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
assert!(!state.bodies.iter().any(|b| b.body.classify_point(Vec3::new(25.0, 25.0, z0 - 0.1)).unwrap() == solidcraft_kernel::PointClass::Inside));
assert!(!state.bodies.iter().any(|b| b.body.classify_point(Vec3::new(25.0, 25.0, z1 + 0.1)).unwrap() == solidcraft_kernel::PointClass::Inside));
assert!(
!state.bodies.iter().any(|b| b.body.classify_point(Vec3::new(10.0, 25.0, (z0 + z1) * 0.5)).unwrap() == solidcraft_kernel::PointClass::Inside)
);
let bounds = state.bodies[0].mesh().bounds();
assert!((bounds.min.z - z0).abs() < 1e-8 && (bounds.max.z - z1).abs() < 1e-8, "{bounds:?}");
}
@@ -2490,8 +2512,8 @@ fn depth_step_reopen(s: &mut Session, suffix: &str, want: f64, inside: solidcraf
let mut imported = Session::default();
run(&mut imported, "doc.open", json!({"path":path.to_string_lossy()}));
assert!((volume(&mut imported) - want).abs() < 1e-6);
assert!(imported.model.state().bodies.iter().any(|b| b.body.contains(inside)));
assert!(!imported.model.state().bodies.iter().any(|b| b.body.contains(outside)));
assert!(imported.model.state().bodies.iter().any(|b| b.body.classify_point(inside).unwrap() == solidcraft_kernel::PointClass::Inside));
assert!(!imported.model.state().bodies.iter().any(|b| b.body.classify_point(outside).unwrap() == solidcraft_kernel::PointClass::Inside));
std::fs::remove_file(path).unwrap();
}
@@ -2583,10 +2605,10 @@ fn partial_depth_intersect_preserves_prior_pockets_and_actual_interval_intersect
assert!((volume(&mut s) - 75.0).abs() < 1e-6);
let state = s.model.state();
let body = &state.bodies[0].body;
assert!(!body.contains(Vec3::new(22.0, 25.0, 0.75)));
assert!(body.contains(Vec3::new(22.0, 25.0, 1.25)));
assert!(body.contains(Vec3::new(27.0, 25.0, 0.75)));
assert!(!body.contains(Vec3::new(27.0, 25.0, 0.25)));
assert_eq!(body.classify_point(Vec3::new(22.0, 25.0, 0.75)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(Vec3::new(22.0, 25.0, 1.25)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(27.0, 25.0, 0.75)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(27.0, 25.0, 0.25)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
let records = s.model.state().sheets[0].pockets.clone();
run(&mut s, "solid.extrude", json!({"sketch":"RetainDepth","distance":1.5,"start_offset":0.25,"operation":"intersect"}));
@@ -2618,9 +2640,9 @@ fn partial_depth_intersect_keeps_disconnected_tool_islands_and_annular_curves()
assert!((volume(&mut s) - 200.0).abs() < 1e-6);
assert_eq!(s.model.state().sheets[0].flat_regions().unwrap().len(), 2);
for x in [25.0, 65.0] {
assert!(s.model.state().bodies[0].body.contains(Vec3::new(x, 25.0, 0.5)));
assert_eq!(s.model.state().bodies[0].body.classify_point(Vec3::new(x, 25.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
}
assert!(!s.model.state().bodies[0].body.contains(Vec3::new(45.0, 25.0, 0.5)));
assert_eq!(s.model.state().bodies[0].body.classify_point(Vec3::new(45.0, 25.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
depth_step_reopen(&mut s, "islands", 200.0, Vec3::new(65.0, 25.0, 0.5), Vec3::new(45.0, 25.0, 0.5));
let mut ring = Session::default();
@@ -2639,9 +2661,9 @@ fn partial_depth_intersect_keeps_disconnected_tool_islands_and_annular_curves()
let area: f64 = state.sheets[0].flat_regions().unwrap().iter().map(|r| r.area()).sum();
assert!((area - 75.0 * PI).abs() < 1e-8);
assert!((volume(&mut ring) - 75.0 * PI).abs() < 0.1);
assert!(state.bodies[0].body.contains(Vec3::new(57.0, 30.0, 0.5)));
assert!(!state.bodies[0].body.contains(Vec3::new(50.0, 30.0, 0.5)));
assert!(!state.bodies[0].body.contains(Vec3::new(57.0, 30.0, 1.5)));
assert_eq!(state.bodies[0].body.classify_point(Vec3::new(57.0, 30.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(state.bodies[0].body.classify_point(Vec3::new(50.0, 30.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(state.bodies[0].body.classify_point(Vec3::new(57.0, 30.0, 1.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
let dxf = run(&mut ring, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap().to_string();
assert!(dxf.contains("POCKET_TOP_Z1.000000_TO_2.500000_MM"));
@@ -2699,9 +2721,9 @@ fn partial_depth_intersect_flipped_tool_and_transferred_sheet_keep_depth_side_an
assert!(state.body(&name).unwrap().transferred);
let world = s.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.25)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.0)));
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 0.25)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 2.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
let bounds = world.body(&name).unwrap().mesh().bounds();
assert!(before.min.dist(bounds.min) < 1e-8 && before.max.dist(bounds.max) < 1e-8);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
@@ -2709,14 +2731,14 @@ fn partial_depth_intersect_flipped_tool_and_transferred_sheet_keep_depth_side_an
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let reopened_world = reopened.world_state().unwrap();
let body = &reopened_world.body(&name).unwrap().body;
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
run(&mut reopened, "timeline.edit", json!({"feature":"RetainSlab","set":{"extent":{"distance":"1.5"}}}));
let world = reopened.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
assert!((solidcraft_kernel::measure(body).unwrap().volume - 150.0).abs() < 1e-6);
assert!(body.contains(Vec3::new(25.0, 25.0, 1.75)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.25)));
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 1.75)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 2.25)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
#[test]
@@ -2767,9 +2789,9 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
let world = s.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
assert!((solidcraft_kernel::measure(body).unwrap().volume - 250.0).abs() < 1e-5);
assert!(body.contains(Vec3::new(25.0, 25.0, 0.1)));
assert!(body.contains(Vec3::new(25.0, 25.0, 2.4)));
assert!(!body.contains(Vec3::new(10.0, 25.0, 1.0)));
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 0.1)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 2.4)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(10.0, 25.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
let mut cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(cold.model.state().sheets[0].pockets.is_empty());
@@ -2784,16 +2806,16 @@ fn partial_depth_intersect_full_gauge_in_rotated_frames_does_not_invent_pockets(
let body = &world.body(&name).unwrap().body;
let measured = solidcraft_kernel::measure(body).unwrap().volume;
assert!((measured - 100.0).abs() < 1e-5, "retained volume {measured}");
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.1)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.4)));
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 0.1)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 2.4)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
let world = cold.world_state().unwrap();
let body = &world.body(&name).unwrap().body;
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-5);
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.1)));
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 0.1)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
}
@@ -2813,9 +2835,9 @@ fn partial_depth_intersect_planar_to_object_retains_current_target_depth() {
let state = s.model.state();
let body = &state.body(&name).unwrap().body;
assert!((solidcraft_kernel::measure(body).unwrap().volume - 100.0).abs() < 1e-6);
assert!(body.contains(Vec3::new(25.0, 25.0, 1.0)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.1)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 2.0)));
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 1.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 0.1)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 2.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(state.sheets[0].pockets.len(), 2);
assert!((solidcraft_kernel::measure(&state.body("Roof").unwrap().body).unwrap().volume - 100.0).abs() < 1e-6);
}
@@ -2860,8 +2882,8 @@ fn partial_depth_intersect_curved_to_object_preserves_current_trimmed_cap_footpr
let ts = tool_session.model.state();
let witness = &ts.body("TrimWitness").unwrap().body;
assert!((solidcraft_kernel::measure(witness).unwrap().volume - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 0.01);
assert!(witness.contains(Vec3::new(21.0, 21.0, 0.5)));
assert!(!witness.contains(Vec3::new(25.0, 25.0, 0.5)));
assert_eq!(witness.classify_point(Vec3::new(21.0, 21.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(witness.classify_point(Vec3::new(25.0, 25.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
run(&mut s, "solid.extrude", json!({"sketch":"Tool","to":[27,25,10],"operation":"intersect","participants":[name]}));
for command in [None, Some("sheet.unfold"), Some("sheet.refold")] {
if let Some(command) = command {
@@ -2873,12 +2895,15 @@ fn partial_depth_intersect_curved_to_object_preserves_current_trimmed_cap_footpr
assert!((measured - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 0.01);
let area: f64 = state.sheets[0].flat_regions().unwrap().iter().map(|r| r.area()).sum();
assert!((area - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 1e-8);
assert!(body.contains(Vec3::new(21.0, 21.0, 0.5)));
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.5)));
assert!(!body.contains(Vec3::new(21.0, 21.0, 1.5)));
assert_eq!(body.classify_point(Vec3::new(21.0, 21.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(25.0, 25.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(body.classify_point(Vec3::new(21.0, 21.0, 1.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
assert!(!cold.model.state().body(&name).unwrap().body.contains(Vec3::new(25.0, 25.0, 0.5)));
assert_eq!(
cold.model.state().body(&name).unwrap().body.classify_point(Vec3::new(25.0, 25.0, 0.5)).unwrap(),
solidcraft_kernel::PointClass::Outside
);
let dxf = run(&mut s, "sheet.export_dxf", json!({"body":name}))["dxf"].as_str().unwrap().to_string();
assert!(dxf.contains("POCKET_TOP_Z1.000000_TO_2.500000_MM"));
let outline = dxf_outline_only(&dxf);
@@ -2888,13 +2913,16 @@ fn partial_depth_intersect_curved_to_object_preserves_current_trimmed_cap_footpr
run(&mut outline_import, "sketch.finish", json!({}));
run(&mut outline_import, "solid.extrude", json!({"distance":1,"profiles":[{"point":[21,21]}]}));
assert!((volume(&mut outline_import) - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 0.01);
assert!(!outline_import.model.state().bodies[0].body.contains(Vec3::new(25.0, 25.0, 0.5)));
assert_eq!(
outline_import.model.state().bodies[0].body.classify_point(Vec3::new(25.0, 25.0, 0.5)).unwrap(),
solidcraft_kernel::PointClass::Outside
);
let path = std::env::temp_dir().join(format!("solidcraft-current-trim-cap-{}.step", std::process::id()));
run(&mut s, "file.export", json!({"path":path.to_string_lossy(),"bodies":[name]}));
let mut imported = Session::default();
run(&mut imported, "doc.open", json!({"path":path.to_string_lossy()}));
assert!((volume(&mut imported) - (100.0 - 4.0 * std::f64::consts::PI)).abs() < 0.01);
assert!(!imported.model.state().bodies[0].body.contains(Vec3::new(25.0, 25.0, 0.5)));
assert_eq!(imported.model.state().bodies[0].body.classify_point(Vec3::new(25.0, 25.0, 0.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
std::fs::remove_file(path).unwrap();
}
@@ -5030,7 +5058,9 @@ fn a_hole_drilled_into_a_rim_is_a_bore_the_sheet_keeps_when_it_unfolds() {
let f = flat(&mut s);
assert_eq!(f["bores"].as_array().map(Vec::len), Some(1), "{f}");
assert_eq!(f["cutouts"], json!([]));
let inside = |s: &Session, p: [f64; 3]| s.model.state().bodies[0].body.contains(solidcraft_geom::Vec3::new(p[0], p[1], p[2]));
let inside = |s: &Session, p: [f64; 3]| {
s.model.state().bodies[0].body.classify_point(solidcraft_geom::Vec3::new(p[0], p[1], p[2])).unwrap() == solidcraft_kernel::PointClass::Inside
};
// The flat pattern loses the same bore (the bends unroll to their own length).
let flat_after = unfolded_volume(&mut s);
assert!(rel(flat_before - flat_after, removed) < 0.02, "flat: removed {} of {removed}", flat_before - flat_after);
@@ -5057,7 +5087,9 @@ fn a_rim_hole_in_a_flange_follows_it_into_the_flat_pattern() {
let t = l_bracket(&mut s);
let before = volume(&mut s);
let flat_before = unfolded_volume(&mut s);
let inside = |s: &Session, p: [f64; 3]| s.model.state().bodies[0].body.contains(solidcraft_geom::Vec3::new(p[0], p[1], p[2]));
let inside = |s: &Session, p: [f64; 3]| {
s.model.state().bodies[0].body.classify_point(solidcraft_geom::Vec3::new(p[0], p[1], p[2])).unwrap() == solidcraft_kernel::PointClass::Inside
};
// Halfway up the flange, in the middle of its thickness (whichever way the rule put it).
let mid = [[40.0 - t / 2.0, 15.0], [40.0 - t / 2.0, -15.0], [40.0 + t / 2.0, 15.0], [40.0 + t / 2.0, -15.0]]
.into_iter()
@@ -5473,7 +5505,7 @@ fn a_mirrored_or_patterned_sheet_body_is_sheet_metal_with_its_flat_pattern() {
(Vec3::new(-99.0, 30.0, 8.0), true),
(Vec3::new(-99.0, 30.0, -8.0), false),
] {
assert_eq!(body.contains(p), inside, "{p:?}");
assert_eq!(body.classify_point(p).unwrap() == solidcraft_kernel::PointClass::Inside, inside, "{p:?}");
}
drop(state);
@@ -5647,9 +5679,12 @@ fn a_mirrored_sheet_copy_is_the_opposite_hand_with_the_mirror_image_flat_pattern
assert_eq!(bends(&mirrored), bends(&original));
assert_eq!(bends(&mirrored).len(), 2);
// The hole is where the mirror put it, on the copy and in its flat pattern.
let in_copy = |p: [f64; 3]| {
s.model.state().bodies.iter().find(|b| b.name == "Body1 (2)").is_some_and(|b| b.body.contains(solidcraft_geom::Vec3::new(p[0], p[1], p[2])))
};
let in_copy =
|p: [f64; 3]| {
s.model.state().bodies.iter().find(|b| b.name == "Body1 (2)").is_some_and(|b| {
b.body.classify_point(solidcraft_geom::Vec3::new(p[0], p[1], p[2])).unwrap() == solidcraft_kernel::PointClass::Inside
})
};
assert!(!in_copy([-25.0, 40.0, 1.0]) && in_copy([-35.0, 40.0, 1.0]));
let hole = flat_points(&mirrored["cutouts"], 1.0);
let cx = hole.iter().map(|p| p.0 as f64).sum::<f64>() / hole.len().max(1) as f64 * 1e-6;
+1 -1
View File
@@ -20,7 +20,7 @@ fn volume(s: &mut Session) -> f64 {
}
fn solid_at(s: &Session, p: Vec3) -> bool {
s.model.state().bodies.iter().any(|b| b.body.contains(p))
s.model.state().bodies.iter().any(|b| b.body.classify_point(p).unwrap() == solidcraft_kernel::PointClass::Inside)
}
/// The circle `c1`'s centre: its point id and where it is in the world.
@@ -152,8 +152,16 @@ fn a_tube_keeps_its_bore_when_its_inner_diameter_changes() {
let volume = m["total"]["volume_mm3"].as_f64().unwrap();
assert!((volume / (100.0 * area) - 1.0).abs() < 0.001, "{m}");
let body = &s.model.state().bodies[0].body;
assert!(!body.contains(solidcraft_geom::Vec3::new(0.0, 0.0, 50.0)), "the bore stays empty");
assert!(body.contains(solidcraft_geom::Vec3::new(inner + 5.0, 0.0, 50.0)), "the wall stays solid");
assert_eq!(
body.classify_point(solidcraft_geom::Vec3::new(0.0, 0.0, 50.0)).unwrap(),
solidcraft_kernel::PointClass::Outside,
"the bore stays empty"
);
assert_eq!(
body.classify_point(solidcraft_geom::Vec3::new(inner + 5.0, 0.0, 50.0)).unwrap(),
solidcraft_kernel::PointClass::Inside,
"the wall stays solid"
);
}
for dimension_first in [false, true] {
let mut s = Session::default();
@@ -93,7 +93,7 @@ fn frames(s: &mut Session, n: usize, live: bool) -> Vec<Value> {
}
fn body_contains(s: &Session, p: Vec3) -> bool {
s.model.state().bodies.first().expect("a body").body.contains(p)
s.model.state().bodies.first().expect("a body").body.classify_point(p).unwrap() == solidcraft_kernel::PointClass::Inside
}
fn pt(s: &Session, id: &str) -> solidcraft_geom::Vec2 {
+8 -2
View File
@@ -1812,13 +1812,19 @@ fn periodic_offset_extrusion_survives_post_extrude_span_changes_and_cold_reopen(
let body = &state.bodies[0].body;
for (distance, material) in [(1.25, true), (0.5, false), (1.75, false)] {
let point = ellipse + normal * distance;
assert_eq!(body.contains(solidcraft_geom::Vec3::new(point.x, point.y, 1.5)), material);
assert_eq!(
body.classify_point(solidcraft_geom::Vec3::new(point.x, point.y, 1.5)).unwrap() == solidcraft_kernel::PointClass::Inside,
material
);
}
// Repeat at the exact major-axis seam as a physical material control.
for (distance, material) in [(1.25, true), (0.5, false), (1.75, false)] {
let point = centre + u * (major + distance);
assert_eq!(ring.region.contains(point), material);
assert_eq!(body.contains(solidcraft_geom::Vec3::new(point.x, point.y, 1.5)), material);
assert_eq!(
body.classify_point(solidcraft_geom::Vec3::new(point.x, point.y, 1.5)).unwrap() == solidcraft_kernel::PointClass::Inside,
material
);
}
let volume = solidcraft_kernel::measure(body).unwrap().volume;
assert!((volume - expected).abs() < 0.05, "{major}: {volume} vs independent {expected}");
+4 -1
View File
@@ -20,7 +20,10 @@ fn volume(s: &Session, body: &str) -> f64 {
}
fn has(s: &Session, body: &str, at: [f64; 3]) -> bool {
s.model.state().body(body).is_some_and(|b| b.body.contains(Vec3::new(at[0], at[1], at[2])))
s.model
.state()
.body(body)
.is_some_and(|b| b.body.classify_point(Vec3::new(at[0], at[1], at[2])).unwrap() == solidcraft_kernel::PointClass::Inside)
}
fn bodies(s: &Session) -> Vec<String> {
+1 -1
View File
@@ -24,7 +24,7 @@ fn volume(s: &mut Session) -> f64 {
/// Whether the model holds material at (x, y, z).
fn solid_at(s: &Session, x: f64, y: f64, z: f64) -> bool {
s.model.state().bodies.iter().any(|b| b.body.contains(Vec3::new(x, y, z)))
s.model.state().bodies.iter().any(|b| b.body.classify_point(Vec3::new(x, y, z)).unwrap() == solidcraft_kernel::PointClass::Inside)
}
/// What the hole feature says (its error and its warning); empty when it is silent.
+2 -2
View File
@@ -34,7 +34,7 @@ fn measure(s: &mut Session) -> Result<(f64, i64), String> {
}
fn solid_at(s: &Session, p: Vec3) -> bool {
s.model.state().bodies.iter().any(|b| b.body.contains(p))
s.model.state().bodies.iter().any(|b| b.body.classify_point(p).unwrap() == solidcraft_kernel::PointClass::Inside)
}
fn v3(a: [f64; 3]) -> Vec3 {
@@ -826,7 +826,7 @@ fn recessed_drill_point_geometry_roundtrip_preview_and_undo() {
p.as_object_mut().unwrap().remove("tip_angle");
let redefine = json!({"feature":hole,"command":"solid.hole","params":p});
let preview = s.preview(&[("timeline.redefine".into(), redefine.clone())]).unwrap();
assert!(preview.after.bodies.iter().any(|b| b.body.contains(probe)));
assert!(preview.after.bodies.iter().any(|b| b.body.classify_point(probe).unwrap() == solidcraft_kernel::PointClass::Inside));
assert_eq!(s.doc.to_json(), saved, "preview cannot accept the bottom change");
run(&mut s, "timeline.redefine", redefine).unwrap();
assert!(solid_at(&s, probe));
+1 -1
View File
@@ -19,7 +19,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
}
fn solid_at(s: &Session, p: Vec3) -> bool {
s.model.state().bodies.iter().any(|b| b.body.contains(p))
s.model.state().bodies.iter().any(|b| b.body.classify_point(p).unwrap() == solidcraft_kernel::PointClass::Inside)
}
/// The bore (5 mm across, 4 mm deep, down from the top face at z = `top`) is about (x, y):
+9 -4
View File
@@ -35,12 +35,17 @@ fn reselect(s: &Session, name: &str) -> Option<String> {
/// Whether any body holds `p`.
fn solid_at(s: &Session, p: [f64; 3]) -> bool {
s.model.state().bodies.iter().any(|b| b.body.contains(Vec3::new(p[0], p[1], p[2])))
s.model.state().bodies.iter().any(|b| b.body.classify_point(Vec3::new(p[0], p[1], p[2])).unwrap() == solidcraft_kernel::PointClass::Inside)
}
/// Which body holds `p`.
fn body_at(s: &Session, p: [f64; 3]) -> Option<String> {
s.model.state().bodies.iter().find(|b| b.body.contains(Vec3::new(p[0], p[1], p[2]))).map(|b| b.name.clone())
s.model
.state()
.bodies
.iter()
.find(|b| b.body.classify_point(Vec3::new(p[0], p[1], p[2])).unwrap() == solidcraft_kernel::PointClass::Inside)
.map(|b| b.name.clone())
}
fn names(s: &Session, body: &str) -> Vec<(String, bool)> {
@@ -149,8 +154,8 @@ fn a_blend_follows_its_named_edge_to_a_moved_lid_instead_of_the_body_below() {
let lid_volume = solidcraft_kernel::measure(&lid.body).unwrap().volume;
assert!((base_volume - 24000.0).abs() < 0.01, "{command}: the base stays sharp: {base_volume}");
assert!((lid_volume - (6000.0 - removed)).abs() < 0.01, "{command}: the lid keeps its blend: {lid_volume}");
assert!(base.body.contains(Vec3::new(39.9, 29.9, 12.5)));
assert!(!lid.body.contains(Vec3::new(39.9, 29.9, 22.5)));
assert_eq!(base.body.classify_point(Vec3::new(39.9, 29.9, 12.5)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(lid.body.classify_point(Vec3::new(39.9, 29.9, 22.5)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
}
+12 -6
View File
@@ -172,10 +172,16 @@ fn join_extrude_from_a_chamfered_top_face() {
assert!((solved.plane.origin.z - height).abs() < 1e-8);
assert!(solved.sketch.links.iter().all(|link| link.auto_face && !link.lost));
let body = &state.bodies[0];
assert!(body.body.contains(solidcraft_geom::Vec3::new(0., 0., height + 4.)));
assert!(!body.body.contains(solidcraft_geom::Vec3::new(0., 0., height + 6.)));
assert!(body.body.contains(solidcraft_geom::Vec3::new(10. - bevel - 0.5, 0., height + 4.)));
assert!(!body.body.contains(solidcraft_geom::Vec3::new(10. - bevel + 0.5, 0., height + 4.)));
assert_eq!(body.body.classify_point(solidcraft_geom::Vec3::new(0., 0., height + 4.)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.body.classify_point(solidcraft_geom::Vec3::new(0., 0., height + 6.)).unwrap(), solidcraft_kernel::PointClass::Outside);
assert_eq!(
body.body.classify_point(solidcraft_geom::Vec3::new(10. - bevel - 0.5, 0., height + 4.)).unwrap(),
solidcraft_kernel::PointClass::Inside
);
assert_eq!(
body.body.classify_point(solidcraft_geom::Vec3::new(10. - bevel + 0.5, 0., height + 4.)).unwrap(),
solidcraft_kernel::PointClass::Outside
);
}
}
}
@@ -1741,7 +1747,7 @@ fn auto_extrude_joins_side_contact_elbow_and_keeps_new_body_explicit() {
assert!(body.validity().is_empty());
assert_eq!(body.lumps().unwrap().len(), 1);
assert_eq!(body.face_count(), 8, "the two flat elbow sides must be single faces");
assert!(body.contains(geom::Vec3::new(5.0, 10.0, 25.0)));
assert_eq!(body.classify_point(geom::Vec3::new(5.0, 10.0, 25.0)).unwrap(), solidcraft_kernel::PointClass::Inside);
}
}
}
@@ -1763,7 +1769,7 @@ fn auto_extrude_joins_corner_screw_boss() {
let body = &st.bodies.first().unwrap().body;
assert_eq!(body.lumps().unwrap().len(), 1);
assert!(body.validity().is_empty());
assert!(!body.contains(geom::Vec3::new(10.0, 10.0, 32.0)), "the screw hole stays open");
assert_eq!(body.classify_point(geom::Vec3::new(10.0, 10.0, 32.0)).unwrap(), solidcraft_kernel::PointClass::Outside, "the screw hole stays open");
}
#[test]
+1 -1
View File
@@ -227,7 +227,7 @@ fn revolve_to_a_cylinder_in_another_moved_component() {
assert!(overlap.is_none_or(|b| solidcraft_kernel::measure(&b).unwrap().volume < 1e-3), "the arc runs into the post");
// Past the picked point's angle (29°) on the inner side, where the post leaves room.
let at = |r: f64, deg: f64| Vec3::new(r * deg.to_radians().cos(), r * deg.to_radians().sin(), -2.5);
assert!(arc.contains(at(10.2, 35.0)), "the arc is not cut back to the post's surface");
assert_eq!(arc.classify_point(at(10.2, 35.0)).unwrap(), solidcraft_kernel::PointClass::Inside, "the arc is not cut back to the post's surface");
}
/// A To Object pick is named after the face it is on, the nearest one, even when a face of a
+2 -2
View File
@@ -28,8 +28,8 @@ fn negative_lens_extrude_cuts_a_partial_sphere_and_reopens() {
let body = &state.bodies[0].body;
assert!(body.validity().is_empty(), "{:?}", body.validity());
assert!(solidcraft_kernel::measure(body).unwrap().bbox.max.z < 20.01, "no lid across the opening");
assert!(body.contains(Vec3::new(0., -10., 2.)));
assert!(!body.contains(Vec3::new(0., -10., 15.)));
assert_eq!(body.classify_point(Vec3::new(0., -10., 2.)).unwrap(), solidcraft_kernel::PointClass::Inside);
assert_eq!(body.classify_point(Vec3::new(0., -10., 15.)).unwrap(), solidcraft_kernel::PointClass::Outside);
};
check(&mut s);
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
+8 -4
View File
@@ -54,7 +54,7 @@ fn tapped_thread_depth_keeps_reference_edge_placement_after_reopen() {
};
assert_eq!(references.len(), 2);
assert_eq!(thread_depth.as_ref().unwrap().to_string(), "3");
assert!(!reopened.model.state().bodies[0].body.contains(Vec3::new(10.0, 12.0, 10.0)));
assert_eq!(reopened.model.state().bodies[0].body.classify_point(Vec3::new(10.0, 12.0, 10.0)).unwrap(), solidcraft_kernel::PointClass::Outside);
}
#[test]
@@ -610,8 +610,11 @@ fn a_mid_face_pick_between_two_free_ends_needs_from() {
let body = &st.body("Part").unwrap().body;
let ring = |z: f64| (0..36).map(|i| (i as f64 * 10.0).to_radians()).map(move |a| Vec3::new(4.9 * a.cos(), 4.9 * a.sin(), z));
let rings = |z0: f64, z1: f64| (0..=40).map(move |i| z0 + (z1 - z0) * i as f64 / 40.0).flat_map(ring);
assert!(rings(0.5, 14.0).all(|q| body.contains(q)), "the bottom half is cut");
assert!(rings(16.0, 29.5).filter(|q| !body.contains(*q)).count() > 100, "the top half is not threaded");
assert!(rings(0.5, 14.0).all(|q| body.classify_point(q).unwrap() == solidcraft_kernel::PointClass::Inside), "the bottom half is cut");
assert!(
rings(16.0, 29.5).filter(|q| body.classify_point(*q).unwrap() == solidcraft_kernel::PointClass::Outside).count() > 100,
"the top half is not threaded"
);
}
/// Up to a plane, or to a point's place along the axis, from the end given.
@@ -741,7 +744,8 @@ fn a_left_handed_thread_turns_the_other_way() {
let a0 = *i as f64 * 10f64.to_radians();
let z0 = 4.5;
let probe = |a: f64| Vec3::new(r * (a0 + a).cos(), r * (a0 + a).sin(), z0 + sign * p * a / std::f64::consts::TAU);
!body.contains(probe(0.0)) && (1..12).all(|k| !body.contains(probe(k as f64 * 0.5)))
body.classify_point(probe(0.0)).unwrap() == solidcraft_kernel::PointClass::Outside
&& (1..12).all(|k| body.classify_point(probe(k as f64 * 0.5)).unwrap() == solidcraft_kernel::PointClass::Outside)
})
.count()
};