diff --git a/crates/engine/src/cmd/face_join_tests.rs b/crates/engine/src/cmd/face_join_tests.rs index 49a5638847..cb6ded041a 100644 --- a/crates/engine/src/cmd/face_join_tests.rs +++ b/crates/engine/src/cmd/face_join_tests.rs @@ -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:?}"); } } diff --git a/crates/engine/src/cmd/face_tests.rs b/crates/engine/src/cmd/face_tests.rs index 3a0e0d9b91..e08ddb66d2 100644 --- a/crates/engine/src/cmd/face_tests.rs +++ b/crates/engine/src/cmd/face_tests.rs @@ -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)); } } diff --git a/crates/engine/src/cmd/model_axis_tests.rs b/crates/engine/src/cmd/model_axis_tests.rs index 54fde4b0d0..60858c0e01 100644 --- a/crates/engine/src/cmd/model_axis_tests.rs +++ b/crates/engine/src/cmd/model_axis_tests.rs @@ -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] diff --git a/crates/engine/src/cmd/patterns_tests.rs b/crates/engine/src/cmd/patterns_tests.rs index e40dfb6812..ce8b06f406 100644 --- a/crates/engine/src/cmd/patterns_tests.rs +++ b/crates/engine/src/cmd/patterns_tests.rs @@ -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(); diff --git a/crates/engine/src/cmd/revolve_convention_tests.rs b/crates/engine/src/cmd/revolve_convention_tests.rs index ed9f5ac5cb..34fa3d3d72 100644 --- a/crates/engine/src/cmd/revolve_convention_tests.rs +++ b/crates/engine/src/cmd/revolve_convention_tests.rs @@ -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}"); diff --git a/crates/engine/src/cmd/sheet_convert_tests.rs b/crates/engine/src/cmd/sheet_convert_tests.rs index 7bf488583d..f768f71465 100644 --- a/crates/engine/src/cmd/sheet_convert_tests.rs +++ b/crates/engine/src/cmd/sheet_convert_tests.rs @@ -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). diff --git a/crates/engine/src/cmd/sheet_tests.rs b/crates/engine/src/cmd/sheet_tests.rs index 627c875913..3bc3b08ac3 100644 --- a/crates/engine/src/cmd/sheet_tests.rs +++ b/crates/engine/src/cmd/sheet_tests.rs @@ -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::() / hole.len().max(1) as f64 * 1e-6; diff --git a/crates/engine/src/cmd/single_hole_tests.rs b/crates/engine/src/cmd/single_hole_tests.rs index e967b55dc6..2d074f97c6 100644 --- a/crates/engine/src/cmd/single_hole_tests.rs +++ b/crates/engine/src/cmd/single_hole_tests.rs @@ -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. diff --git a/crates/engine/src/cmd/sketch_circle_edge_tests.rs b/crates/engine/src/cmd/sketch_circle_edge_tests.rs index b59961c243..5ed440a2f4 100644 --- a/crates/engine/src/cmd/sketch_circle_edge_tests.rs +++ b/crates/engine/src/cmd/sketch_circle_edge_tests.rs @@ -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(); diff --git a/crates/engine/src/cmd/sketch_drag_live_tests.rs b/crates/engine/src/cmd/sketch_drag_live_tests.rs index bb1c7d7953..e83e138dd8 100644 --- a/crates/engine/src/cmd/sketch_drag_live_tests.rs +++ b/crates/engine/src/cmd/sketch_drag_live_tests.rs @@ -93,7 +93,7 @@ fn frames(s: &mut Session, n: usize, live: bool) -> Vec { } 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 { diff --git a/crates/engine/src/cmd/sketch_modify_tests.rs b/crates/engine/src/cmd/sketch_modify_tests.rs index 08ef485ebe..41fb2d258d 100644 --- a/crates/engine/src/cmd/sketch_modify_tests.rs +++ b/crates/engine/src/cmd/sketch_modify_tests.rs @@ -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}"); diff --git a/crates/engine/src/exact_tool_tests.rs b/crates/engine/src/exact_tool_tests.rs index dc29b3c4c1..564f81a066 100644 --- a/crates/engine/src/exact_tool_tests.rs +++ b/crates/engine/src/exact_tool_tests.rs @@ -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 { diff --git a/crates/engine/src/hole_face_tests.rs b/crates/engine/src/hole_face_tests.rs index 9bcb515bd1..f165a224b2 100644 --- a/crates/engine/src/hole_face_tests.rs +++ b/crates/engine/src/hole_face_tests.rs @@ -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. diff --git a/crates/engine/src/hole_matrix_tests.rs b/crates/engine/src/hole_matrix_tests.rs index 5898647ee4..107ff8514a 100644 --- a/crates/engine/src/hole_matrix_tests.rs +++ b/crates/engine/src/hole_matrix_tests.rs @@ -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)); diff --git a/crates/engine/src/hole_reference_tests.rs b/crates/engine/src/hole_reference_tests.rs index da8ac53a67..38ab86424b 100644 --- a/crates/engine/src/hole_reference_tests.rs +++ b/crates/engine/src/hole_reference_tests.rs @@ -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): diff --git a/crates/engine/src/naming_history_tests.rs b/crates/engine/src/naming_history_tests.rs index fe6c3d1329..fe531a30f2 100644 --- a/crates/engine/src/naming_history_tests.rs +++ b/crates/engine/src/naming_history_tests.rs @@ -35,12 +35,17 @@ fn reselect(s: &Session, name: &str) -> Option { /// 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 { - 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); } } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index b22e99c91f..e27f337824 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -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] diff --git a/crates/engine/src/tests/component_frames.rs b/crates/engine/src/tests/component_frames.rs index 48d3ca62c8..88819dfc1e 100644 --- a/crates/engine/src/tests/component_frames.rs +++ b/crates/engine/src/tests/component_frames.rs @@ -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 diff --git a/crates/engine/src/tests/sphere_lens.rs b/crates/engine/src/tests/sphere_lens.rs index de06a19a9f..394227fa78 100644 --- a/crates/engine/src/tests/sphere_lens.rs +++ b/crates/engine/src/tests/sphere_lens.rs @@ -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()); diff --git a/crates/engine/src/thread_tests.rs b/crates/engine/src/thread_tests.rs index 755a98a2c1..b1a8cc0729 100644 --- a/crates/engine/src/thread_tests.rs +++ b/crates/engine/src/thread_tests.rs @@ -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() };