#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::f64::consts::PI;
use brepkit_check::classify::{ClassifyOptions, PointClassification, classify_point};
use brepkit_math::mat::Mat4;
use brepkit_math::vec::Point3;
use brepkit_operations::boolean::{BooleanOp, boolean};
use brepkit_operations::measure::solid_volume;
use brepkit_operations::primitives::{make_box, make_sphere};
use brepkit_operations::transform::transform_solid;
use brepkit_operations::validate::validate_solid;
use brepkit_topology::Topology;
use brepkit_topology::explorer::solid_faces;
use brepkit_topology::solid::SolidId;
const RADIUS: f64 = 3.0;
fn hollow_cube(topo: &mut Topology) -> SolidId {
let cube = make_box(topo, 20.0, 20.0, 20.0).unwrap();
transform_solid(topo, cube, &Mat4::translation(-10.0, -10.0, -10.0)).unwrap();
let ball = make_sphere(topo, RADIUS, 32).unwrap();
boolean(topo, BooleanOp::Cut, cube, ball).unwrap()
}
fn ball_above(z0: f64) -> f64 {
let h = (RADIUS - z0).clamp(0.0, 2.0 * RADIUS);
PI * h * h * (3.0 * RADIUS - h) / 3.0
}
fn check(topo: &Topology, result: SolidId, truth: f64, label: &str) {
let faces = solid_faces(topo, result).unwrap().len();
assert!(faces <= 16, "{label}: fell back to a mesh ({faces} faces)");
let report = validate_solid(topo, result).unwrap();
assert!(report.is_valid(), "{label}: {:?}", report.issues);
let volume = solid_volume(topo, result, 0.01).unwrap();
assert!(
(volume - truth).abs() < 1e-6 * truth,
"{label}: volume {volume}, truth {truth}"
);
}
#[test]
fn a_cavity_survives_booleans_with_a_box_below_a_plane() {
let ball = 4.0 / 3.0 * PI * RADIUS.powi(3);
for z0 in [-4.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.5] {
let above = ball_above(z0);
let below = ball - above;
let slab = 400.0 * (z0 + 10.0);
for (op, swapped, truth) in [
(BooleanOp::Intersect, false, slab - below),
(BooleanOp::Cut, false, 8000.0 - slab - above),
(BooleanOp::Cut, true, below),
(BooleanOp::Fuse, false, 8000.0 - above),
] {
if truth < 1e-9 {
continue;
}
let label = format!("z < {z0}: {op:?}{}", if swapped { " swapped" } else { "" });
let mut topo = Topology::new();
let hollow = hollow_cube(&mut topo);
let other = make_box(&mut topo, 20.0, 20.0, z0 + 10.0).unwrap();
transform_solid(&mut topo, other, &Mat4::translation(-10.0, -10.0, -10.0)).unwrap();
let result = if swapped {
boolean(&mut topo, op, other, hollow)
} else {
boolean(&mut topo, op, hollow, other)
}
.unwrap();
check(&topo, result, truth, &label);
}
}
}
#[test]
fn a_cavity_survives_booleans_with_a_box_holding_it() {
let ball = 4.0 / 3.0 * PI * RADIUS.powi(3);
for (op, swapped, truth, centre) in [
(
BooleanOp::Intersect,
false,
7600.0 - ball,
PointClassification::Outside,
),
(BooleanOp::Cut, false, 400.0, PointClassification::Outside),
(
BooleanOp::Cut,
true,
400.0 + ball,
PointClassification::Inside,
),
(BooleanOp::Fuse, false, 8400.0, PointClassification::Inside),
] {
let label = format!("{op:?}{}", if swapped { " swapped" } else { "" });
let mut topo = Topology::new();
let hollow = hollow_cube(&mut topo);
let other = make_box(&mut topo, 20.0, 20.0, 20.0).unwrap();
transform_solid(&mut topo, other, &Mat4::translation(-9.0, -10.0, -10.0)).unwrap();
let result = if swapped {
boolean(&mut topo, op, other, hollow)
} else {
boolean(&mut topo, op, hollow, other)
}
.unwrap();
check(&topo, result, truth, &label);
let got = classify_point(
&topo,
result,
Point3::new(0.3, 0.2, -1.0),
&ClassifyOptions::default(),
)
.unwrap();
assert_eq!(got, centre, "{label}: beside the cavity's centre");
}
}
#[test]
fn a_box_swallowing_the_cavity_is_not_held_by_the_cube() {
let ball = 4.0 / 3.0 * PI * RADIUS.powi(3);
let block = 13.4 * 7.0 * 7.0;
for (op, truth) in [
(BooleanOp::Intersect, block - ball),
(BooleanOp::Fuse, 8000.0),
(BooleanOp::Cut, 8000.0 - block),
] {
let label = format!("{op:?}");
let mut topo = Topology::new();
let hollow = hollow_cube(&mut topo);
let other = make_box(&mut topo, 13.4, 7.0, 7.0).unwrap();
transform_solid(&mut topo, other, &Mat4::translation(-3.5, -3.5, -3.5)).unwrap();
let result = boolean(&mut topo, op, hollow, other).unwrap();
check(&topo, result, truth, &label);
}
}
#[test]
fn a_cap_cavity_keeps_its_side_with_its_disc_through_the_origin() {
let ball = 4.0 / 3.0 * PI * RADIUS.powi(3);
let (above, slab) = (ball_above(1.0), 400.0 * 11.0);
let down = Mat4::translation(0.0, 0.0, -1.0);
for (op, swapped, truth) in [
(BooleanOp::Intersect, false, slab - (ball - above)),
(BooleanOp::Cut, false, 8000.0 - slab - above),
(BooleanOp::Cut, true, ball - above),
(BooleanOp::Fuse, false, 8000.0 - above),
] {
let label = format!("{op:?}{}", if swapped { " swapped" } else { "" });
let mut topo = Topology::new();
let hollow = hollow_cube(&mut topo);
transform_solid(&mut topo, hollow, &down).unwrap();
let other = make_box(&mut topo, 20.0, 20.0, 11.0).unwrap();
transform_solid(
&mut topo,
other,
&(down * Mat4::translation(-10.0, -10.0, -10.0)),
)
.unwrap();
let result = if swapped {
boolean(&mut topo, op, other, hollow)
} else {
boolean(&mut topo, op, hollow, other)
}
.unwrap();
check(&topo, result, truth, &label);
}
}