use cadrum::{Compound, Solid};
use glam::DVec3;
fn dvec3(x: f64, y: f64, z: f64) -> DVec3 {
DVec3::new(x, y, z)
}
fn test_box() -> Solid {
Solid::cube(10.0, 10.0, 10.0)
}
#[test]
fn test_translated_preserves_volume() {
let shape = test_box();
let moved = shape.translate(dvec3(100.0, 200.0, -50.0));
assert!((moved.volume() - 1000.0).abs() < 1e-6);
}
#[test]
fn test_union_of_translated_overlapping_solids_has_single_volume() {
let a = [Solid::cube(10.0, 10.0, 10.0)];
let b = [Solid::cube(10.0, 10.0, 10.0).translate(dvec3(100.0, 0.0, 0.0))];
let b_moved: Vec<Solid> = b.clone().into_iter().map(|s| s.translate(dvec3(-100.0, 0.0, 0.0))).collect();
let result_no_move: Vec<Solid> = a.union(&b).expect("union should succeed");
let volume_no_move: f64 = result_no_move.iter().map(|s| s.volume()).sum();
assert!((volume_no_move - 2000.0).abs() < 1e-3, "expected volume ~2000, got {volume_no_move}");
let result: Vec<Solid> = a.union(&b_moved).expect("union should succeed");
let volume: f64 = result.iter().map(|s| s.volume()).sum();
assert!((volume - 1000.0).abs() < 1e-3, "expected volume ~1000, got {volume}");
let result_with_b: Vec<Solid> = result.union(&b).expect("union should succeed");
let volume_with_b: f64 = result_with_b.iter().map(|s| s.volume()).sum();
assert!((volume_with_b - 2000.0).abs() < 1e-3, "expected volume ~2000, got {volume_with_b}");
}
#[test]
fn test_rotated_preserves_volume() {
let shape = test_box();
let rotated = shape.rotate_z(std::f64::consts::FRAC_PI_4);
assert!((rotated.volume() - 1000.0).abs() < 1e-3);
}
#[test]
fn test_rotated_full_turn_preserves_volume() {
let shape = test_box();
let rotated = shape.rotate_z(std::f64::consts::TAU);
assert!((rotated.volume() - 1000.0).abs() < 1e-3);
}
#[test]
fn test_rotated_y_preserves_volume() {
let shape = test_box();
let rotated = shape.rotate_y(std::f64::consts::FRAC_PI_2);
assert!((rotated.volume() - 1000.0).abs() < 1e-3);
}
#[test]
fn test_scale_volume() {
let shape = test_box();
let scaled = shape.scale(DVec3::ZERO, 2.0);
assert!((scaled.volume() - 8000.0).abs() < 1e-3);
}
#[test]
fn test_scale_half_volume() {
let shape = test_box();
let scaled = shape.scale(DVec3::ZERO, 0.5);
assert!((scaled.volume() - 125.0).abs() < 1e-3);
}
#[test]
fn test_scale_triple_volume() {
let shape = test_box();
let scaled = shape.scale(DVec3::ZERO, 3.0);
assert!((scaled.volume() - 27_000.0).abs() < 1e-3);
}
#[test]
fn test_preserves_face_ids() {
fn face_ids<'a>(s: impl IntoIterator<Item = &'a Solid>) -> Vec<u64> {
s.into_iter().flat_map(|s| s.iter_face()).map(|f| f.id()).collect()
}
let shape = test_box();
let solid_id = shape.id();
let ids = face_ids([&shape]);
let moved = shape.translate(dvec3(10.0, 0.0, 0.0));
assert_eq!(solid_id, moved.id(), "translate should preserve solid tshape_id");
assert_eq!(ids, face_ids([&moved]), "translate should preserve face IDs");
let shape = test_box();
let solid_id = shape.id();
let ids = face_ids([&shape]);
let rotated = shape.rotate_z(std::f64::consts::FRAC_PI_4);
assert_eq!(solid_id, rotated.id(), "rotate should preserve solid tshape_id");
assert_eq!(ids, face_ids([&rotated]), "rotate should preserve face IDs");
}
#[test]
fn test_new_faces_subtract_b_inside_a() {
let big = [Solid::cube(10.0, 10.0, 10.0)];
let small = [Solid::cube(4.0, 4.0, 4.0).translate(dvec3(3.0, 3.0, 3.0))];
let small_face_ids: std::collections::HashSet<u64> =
small.iter().flat_map(|s| s.iter_face()).map(|f| f.id()).collect();
let solids = big.subtract(&small).unwrap();
let tool_post_ids: std::collections::HashSet<u64> = solids.iter()
.flat_map(|s| s.iter_history())
.filter_map(|[post, src]| small_face_ids.contains(&src).then_some(post))
.collect();
assert_eq!(
solids.iter().flat_map(|s| s.iter_face()).filter(|f| tool_post_ids.contains(&f.id())).count(),
6,
"subtract with B fully inside A: tool faces should be all 6 inner walls"
);
}
#[test]
fn test_new_faces_intersect_b_inside_a() {
let big = [Solid::cube(10.0, 10.0, 10.0)];
let small = [Solid::cube(4.0, 4.0, 4.0).translate(dvec3(3.0, 3.0, 3.0))];
let small_face_ids: std::collections::HashSet<u64> =
small.iter().flat_map(|s| s.iter_face()).map(|f| f.id()).collect();
let solids = big.intersect(&small).unwrap();
let tool_post_ids: std::collections::HashSet<u64> = solids.iter()
.flat_map(|s| s.iter_history())
.filter_map(|[post, src]| small_face_ids.contains(&src).then_some(post))
.collect();
let tool_count = solids.iter().flat_map(|s| s.iter_face()).filter(|f| tool_post_ids.contains(&f.id())).count();
assert_eq!(tool_count, 6, "intersect with B fully inside A: tool faces should equal all faces of result");
assert_eq!(solids.iter().flat_map(|s| s.iter_face()).count(), tool_count, "intersect with B fully inside A: tool faces should cover all result faces");
}
#[test]
fn test_bounding_box() {
let [min, max] = Solid::cube(3.0, 4.0, 5.0).translate(dvec3(1.0, 2.0, 3.0)).bounding_box();
assert!((min - dvec3(1.0, 2.0, 3.0)).length() < 1e-6);
assert!((max - dvec3(4.0, 6.0, 8.0)).length() < 1e-6);
let solids = [Solid::cube(2.0, 2.0, 2.0), Solid::cube(2.0, 3.0, 4.0).translate(dvec3(5.0, 5.0, 5.0))];
let bboxes: Vec<[DVec3; 2]> = solids.iter().map(|s| s.bounding_box()).collect();
let min = bboxes.iter().map(|b| b[0]).reduce(|a, b| a.min(b)).unwrap();
let max = bboxes.iter().map(|b| b[1]).reduce(|a, b| a.max(b)).unwrap();
assert!((min - dvec3(0.0, 0.0, 0.0)).length() < 1e-6);
assert!((max - dvec3(7.0, 8.0, 9.0)).length() < 1e-6);
let moved = test_box().translate(dvec3(10.0, 20.0, 30.0));
let [min, max] = moved.bounding_box();
assert!((min - dvec3(10.0, 20.0, 30.0)).length() < 1e-6);
assert!((max - dvec3(20.0, 30.0, 40.0)).length() < 1e-6);
let [min, max] = Solid::half_space(dvec3(0.0, 0.0, 0.0), dvec3(0.0, 0.0, 1.0)).bounding_box();
println!("half_space bbox: min={min:?} max={max:?}");
}
#[test]
fn test_contains() {
let shape = test_box(); assert!(shape.contains(dvec3(5.0, 5.0, 5.0))); assert!(shape.contains(dvec3(0.1, 0.1, 0.1))); assert!(!shape.contains(dvec3(20.0, 5.0, 5.0))); assert!(!shape.contains(dvec3(-0.1, 5.0, 5.0))); }