use super::*;
use crate::{chamfer_edge, fillet_edge, make_box_brep, make_cylinder_brep};
fn unit_cube() -> BrepSolid {
make_box_brep(Vec3::new(0.0, 0.0, 0.0), 1.0, 1.0, 1.0).unwrap()
}
fn edge_id_near(solid: &BrepSolid, target: Vec3) -> u64 {
solid
.edges
.iter()
.min_by(|a, b| {
let ma = a
.curve
.evaluate((a.t0 + a.t1) * 0.5)
.unwrap()
.sub(target)
.length();
let mb = b
.curve
.evaluate((b.t0 + b.t1) * 0.5)
.unwrap()
.sub(target)
.length();
ma.partial_cmp(&mb).unwrap()
})
.unwrap()
.id
}
#[test]
fn deletes_chamfer_and_recovers_the_sharp_cube() {
let cube = unit_cube();
let volume = solid_signed_volume(&cube).unwrap().abs();
let edge = edge_id_near(&cube, Vec3::new(1.0, 0.5, 1.0));
let chamfered = chamfer_edge(&cube, edge, 0.2, Some("chamfer")).unwrap();
assert!(chamfered.validate().is_empty());
let chamfer_face = resolve_face_by_point(&chamfered, Vec3::new(0.9, 0.5, 0.9)).unwrap();
let face_count_before = chamfered.shells[0].faces.len();
let healed = delete_face_and_heal(&chamfered, chamfer_face).unwrap();
assert!(
healed.validate().is_empty(),
"healed solid must validate: {:?}",
healed.validate()
);
assert_eq!(healed.shells[0].faces.len(), face_count_before - 1);
assert!(!healed.shells[0].faces.iter().any(|f| f.id == chamfer_face));
let healed_volume = solid_signed_volume(&healed).unwrap().abs();
assert!(
(healed_volume - volume).abs() < 1e-6,
"expected full-cube volume {volume}, got {healed_volume}"
);
assert!(healed.edges.iter().any(|edge| {
let mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).unwrap();
(mid.x - 1.0).abs() < 1e-6 && (mid.z - 1.0).abs() < 1e-6
}));
}
#[test]
fn deletes_fillet_and_recovers_the_sharp_cube() {
let cube = unit_cube();
let volume = solid_signed_volume(&cube).unwrap().abs();
let edge = edge_id_near(&cube, Vec3::new(1.0, 0.5, 1.0));
let filleted = fillet_edge(&cube, edge, 0.2, Some("fillet")).unwrap();
assert!(filleted.validate().is_empty());
let offset = 0.2 / 2.0_f64.sqrt();
let probe = Vec3::new(0.8 + offset, 0.5, 0.8 + offset);
let fillet_face = resolve_face_by_point(&filleted, probe).unwrap();
let face_count_before = filleted.shells[0].faces.len();
let healed = delete_face_and_heal(&filleted, fillet_face).unwrap();
assert!(
healed.validate().is_empty(),
"healed solid must validate: {:?}",
healed.validate()
);
assert_eq!(healed.shells[0].faces.len(), face_count_before - 1);
let healed_volume = solid_signed_volume(&healed).unwrap().abs();
assert!(
(healed_volume - volume).abs() < 1e-6,
"expected full-cube volume {volume}, got {healed_volume}"
);
assert!(healed.edges.iter().any(|edge| {
let mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5).unwrap();
(mid.x - 1.0).abs() < 1e-6 && (mid.z - 1.0).abs() < 1e-6
}));
}
#[test]
fn refuses_to_delete_a_face_whose_neighbours_cannot_reintersect() {
let cube = unit_cube();
let top = resolve_face_by_point(&cube, Vec3::new(0.5, 0.5, 1.0)).unwrap();
let result = delete_face_and_heal(&cube, top);
assert!(result.is_err(), "expected a refusal, got a solid");
let message = result.unwrap_err();
assert!(
message.contains("re-intersect") || message.contains("parallel"),
"unexpected error: {message}"
);
assert!(cube.validate().is_empty());
}
#[test]
fn moving_a_box_face_outward_grows_volume_like_an_extrude() {
let block = make_box_brep(Vec3::new(0.0, 0.0, 0.0), 1.0, 2.0, 3.0).unwrap();
let volume = solid_signed_volume(&block).unwrap();
let plus_x = resolve_face_by_point(&block, Vec3::new(1.0, 1.0, 1.5)).unwrap();
let moved = move_faces(&block, &[plus_x], Vec3::new(0.5, 0.0, 0.0)).unwrap();
assert!(
moved.validate().is_empty(),
"moved solid must validate: {:?}",
moved.validate()
);
let moved_volume = solid_signed_volume(&moved).unwrap();
assert!(
(moved_volume - (volume + 0.5 * 2.0 * 3.0)).abs() < 1e-9,
"expected {} + 3, got {moved_volume}",
volume
);
assert_eq!(moved.vertices.len(), block.vertices.len());
assert_eq!(moved.edges.len(), block.edges.len());
assert_eq!(moved.shells[0].faces.len(), block.shells[0].faces.len());
assert!(block.validate().is_empty());
assert!((solid_signed_volume(&block).unwrap() - volume).abs() < 1e-12);
}
#[test]
fn moving_a_box_face_inward_shrinks_volume_exactly() {
let block = make_box_brep(Vec3::new(0.0, 0.0, 0.0), 1.0, 2.0, 3.0).unwrap();
let volume = solid_signed_volume(&block).unwrap();
let plus_x = resolve_face_by_point(&block, Vec3::new(1.0, 1.0, 1.5)).unwrap();
let moved = move_faces(&block, &[plus_x], Vec3::new(-0.25, 0.0, 0.0)).unwrap();
assert!(moved.validate().is_empty());
let moved_volume = solid_signed_volume(&moved).unwrap();
assert!(
(moved_volume - (volume - 0.25 * 2.0 * 3.0)).abs() < 1e-9,
"expected {} - 1.5, got {moved_volume}",
volume
);
}
#[test]
fn moving_a_two_face_group_diagonally_yields_the_analytic_prism() {
let cube = unit_cube();
let plus_x = resolve_face_by_point(&cube, Vec3::new(1.0, 0.5, 0.5)).unwrap();
let plus_y = resolve_face_by_point(&cube, Vec3::new(0.5, 1.0, 0.5)).unwrap();
let moved = move_faces(&cube, &[plus_x, plus_y], Vec3::new(0.3, 0.4, 0.0)).unwrap();
assert!(
moved.validate().is_empty(),
"moved solid must validate: {:?}",
moved.validate()
);
let moved_volume = solid_signed_volume(&moved).unwrap();
assert!(
(moved_volume - 1.82).abs() < 1e-9,
"expected the analytic prism volume 1.82, got {moved_volume}"
);
assert!(moved
.vertices
.iter()
.any(|vertex| { vertex.point.sub(Vec3::new(1.3, 1.4, 1.0)).length() < 1e-9 }));
assert_eq!(moved.vertices.len(), 8);
assert_eq!(moved.edges.len(), 12);
assert_eq!(moved.shells[0].faces.len(), 6);
}
#[test]
fn moving_a_cylinder_cap_is_refused_honestly_in_the_planar_neighbour_slice() {
let cylinder =
make_cylinder_brep(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 1.0, 2.0).unwrap();
let cap = resolve_face_by_point(&cylinder, Vec3::new(0.0, 0.0, 2.0)).unwrap();
let result = move_faces(&cylinder, &[cap], Vec3::new(0.0, 0.0, 0.5));
let message = result.unwrap_err();
assert!(
message.starts_with("move_faces:"),
"refusal must carry the operation name: {message}"
);
assert!(
message.contains("not planar"),
"refusal must name the curved-neighbour limitation: {message}"
);
assert!(cylinder.validate().is_empty());
}
#[test]
fn unknown_ids_and_collapsing_translations_are_refused_without_panic() {
let cube = unit_cube();
let step = Vec3::new(0.1, 0.0, 0.0);
let unknown = move_faces(&cube, &[424_242], step).unwrap_err();
assert!(unknown.contains("no face with id"), "got: {unknown}");
let empty = move_faces(&cube, &[], step).unwrap_err();
assert!(empty.contains("no faces selected"), "got: {empty}");
let plus_x = resolve_face_by_point(&cube, Vec3::new(1.0, 0.5, 0.5)).unwrap();
let collapse = move_faces(&cube, &[plus_x], Vec3::new(-1.0, 0.0, 0.0)).unwrap_err();
assert!(collapse.contains("collapses"), "got: {collapse}");
let invert = move_faces(&cube, &[plus_x], Vec3::new(-1.5, 0.0, 0.0)).unwrap_err();
assert!(invert.contains("inverts"), "got: {invert}");
assert!(cube.validate().is_empty());
assert!((solid_signed_volume(&cube).unwrap() - 1.0).abs() < 1e-12);
}