#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::f64::consts::PI;
use brepkit_check::classify::{ClassifyOptions, PointClassification, classify_point};
use brepkit_math::mat::Mat4;
use brepkit_math::vec::{Point3, Vec3};
use brepkit_operations::boolean::{BooleanOp, boolean};
use brepkit_operations::measure::solid_volume;
use brepkit_operations::offset_v2::offset_solid_v2;
use brepkit_operations::primitives::{make_box, make_cone, make_cylinder, make_sphere, make_torus};
use brepkit_operations::shell_op::shell;
use brepkit_operations::tessellate::{is_watertight, tessellate_solid};
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::face::{FaceId, FaceSurface};
use brepkit_topology::solid::SolidId;
type Make = fn(&mut Topology) -> SolidId;
type Probes = (&'static [(f64, f64, f64)], &'static [(f64, f64, f64)]);
fn assert_material(topo: &Topology, solid: SolidId, place: &Mat4, probes: Probes, label: &str) {
let (inside, outside) = probes;
for (points, want) in [
(inside, PointClassification::Inside),
(outside, PointClassification::Outside),
] {
for &(x, y, z) in points {
let p = place.mul_point(Point3::new(x, y, z));
let got = classify_point(topo, solid, p, &ClassifyOptions::default()).unwrap();
assert_eq!(got, want, "{label}: ({x}, {y}, {z})");
}
}
}
fn frustum(h: f64, a: f64, b: f64) -> f64 {
PI * h / 3.0 * b.mul_add(b, a.mul_add(a, a * b))
}
fn cone_shift() -> f64 {
1.09_f64.sqrt()
}
fn poses() -> [(&'static str, Mat4); 3] {
[
("upright", Mat4::identity()),
(
"turned",
Mat4::translation(1.5, -2.0, 0.7) * Mat4::rotation_x(0.7) * Mat4::rotation_z(0.4),
),
("mirrored", Mat4::scale(-1.0, 1.0, 1.0)),
]
}
fn assert_exact(topo: &Topology, solid: SolidId, truth: f64, label: &str) {
let report = validate_solid(topo, solid).unwrap();
assert!(report.is_valid(), "{label}: {:?}", report.issues);
let mesh = tessellate_solid(topo, solid, 0.01).unwrap();
assert!(is_watertight(&mesh), "{label}: open or non-manifold mesh");
for f in solid_faces(topo, solid).unwrap() {
let surface = topo.face(f).unwrap().surface();
assert!(
!matches!(surface, FaceSurface::Nurbs(_)),
"{label}: a face was refit as NURBS"
);
}
let volume = solid_volume(topo, solid, 0.01).unwrap();
assert!(
(volume - truth).abs() < 1e-9 * truth,
"{label}: volume {volume}, truth {truth}"
);
}
fn top_face(topo: &Topology, solid: SolidId, up: Vec3) -> FaceId {
let mut best: Option<(f64, FaceId)> = None;
for f in solid_faces(topo, solid).unwrap() {
let face = topo.face(f).unwrap();
if let FaceSurface::Plane { normal, d } = face.surface() {
let n = if face.is_reversed() {
-*normal
} else {
*normal
};
if n.dot(up) > 0.9 {
let height = d * normal.dot(up);
if best.is_none_or(|(h, _)| height > h) {
best = Some((height, f));
}
}
}
}
best.unwrap().1
}
#[test]
fn primitives_offset_exactly() {
let k = cone_shift();
let cases: [(&str, Make, f64, f64); 5] = [
(
"box",
|t| make_box(t, 10.0, 10.0, 10.0).unwrap(),
512.0,
1728.0,
),
(
"cylinder",
|t| make_cylinder(t, 5.0, 10.0).unwrap(),
PI * 16.0 * 8.0,
PI * 36.0 * 12.0,
),
(
"frustum",
|t| make_cone(t, 5.0, 2.0, 10.0).unwrap(),
frustum(8.0, 4.7 - k, 2.3 - k),
frustum(12.0, 5.3 + k, 1.7 + k),
),
(
"sphere",
|t| make_sphere(t, 5.0, 32).unwrap(),
4.0 / 3.0 * PI * 64.0,
4.0 / 3.0 * PI * 216.0,
),
(
"torus",
|t| make_torus(t, 6.0, 2.0, 32).unwrap(),
2.0 * PI * PI * 6.0,
2.0 * PI * PI * 54.0,
),
];
for (name, make, inward, outward) in cases {
for (pose, place) in poses() {
for (distance, truth) in [(-1.0, inward), (1.0, outward)] {
let mut topo = Topology::new();
let solid = make(&mut topo);
transform_solid(&mut topo, solid, &place).unwrap();
let offset = offset_solid_v2(&mut topo, solid, distance).unwrap();
assert_exact(&topo, offset, truth, &format!("{name} {pose} {distance}"));
}
}
}
}
#[test]
fn concave_and_holed_solids_offset_exactly() {
let ell: Make = |t| {
let a = make_box(t, 10.0, 4.0, 4.0).unwrap();
let b = make_box(t, 4.0, 10.0, 4.0).unwrap();
boolean(t, BooleanOp::Fuse, a, b).unwrap()
};
let plate: Make = |t| {
let b = make_box(t, 10.0, 10.0, 4.0).unwrap();
let c = make_cylinder(t, 2.0, 10.0).unwrap();
transform_solid(t, c, &Mat4::translation(5.0, 5.0, -3.0)).unwrap();
boolean(t, BooleanOp::Cut, b, c).unwrap()
};
let cases: [(&str, Make, f64, f64, Probes); 4] = [
(
"ell",
ell,
-1.0,
2.0 * 8.0 * 2.0 * 2.0 - 2.0 * 2.0 * 2.0,
(
&[(2.0, 2.0, 2.0), (8.0, 2.0, 2.0)],
&[(0.5, 2.0, 2.0), (5.0, 5.0, 2.0)],
),
),
(
"ell",
ell,
1.0,
2.0 * 12.0 * 6.0 * 6.0 - 6.0 * 6.0 * 6.0,
(
&[(-0.5, 2.0, 2.0), (4.5, 4.5, 2.0)],
&[(6.0, 6.0, 2.0), (-1.5, 2.0, 2.0)],
),
),
(
"plate",
plate,
-0.5,
9.0 * 9.0 * 3.0 - PI * 2.5 * 2.5 * 3.0,
(&[(5.0, 7.7, 2.0)], &[(5.0, 7.3, 2.0), (0.3, 2.0, 2.0)]),
),
(
"plate",
plate,
0.5,
11.0 * 11.0 * 5.0 - PI * 1.5 * 1.5 * 5.0,
(&[(5.0, 6.7, 2.0), (-0.3, 2.0, 4.3)], &[(5.0, 6.3, 2.0)]),
),
];
for (name, make, distance, truth, probes) in cases {
for (pose, place) in poses() {
let mut topo = Topology::new();
let solid = make(&mut topo);
transform_solid(&mut topo, solid, &place).unwrap();
let offset = offset_solid_v2(&mut topo, solid, distance).unwrap();
let label = format!("{name} {pose} {distance}");
assert_exact(&topo, offset, truth, &label);
assert_material(&topo, offset, &place, probes, &label);
}
}
}
#[test]
fn curved_solids_shell_exactly() {
let k = cone_shift();
let cases: [(&str, Make, bool, f64, Probes); 4] = [
(
"cylinder",
|t| make_cylinder(t, 5.0, 10.0).unwrap(),
true,
PI * 25.0 * 10.0 - PI * 16.0 * 9.0,
(
&[(4.5, 0.0, 5.0), (0.0, 0.0, 0.5)],
&[(0.0, 0.0, 5.0), (5.5, 0.0, 5.0)],
),
),
(
"frustum",
|t| make_cone(t, 5.0, 2.0, 10.0).unwrap(),
true,
frustum(10.0, 5.0, 2.0) - frustum(9.0, 4.7 - k, 2.0 - k),
(&[(0.0, 0.0, 0.5)], &[(0.0, 0.0, 5.0)]),
),
(
"sphere",
|t| make_sphere(t, 5.0, 32).unwrap(),
false,
4.0 / 3.0 * PI * 61.0,
(&[(4.5, 0.0, 0.3)], &[(0.0, 0.0, 0.3), (5.5, 0.0, 0.3)]),
),
(
"torus",
|t| make_torus(t, 6.0, 2.0, 32).unwrap(),
false,
2.0 * PI * PI * 18.0,
(&[(7.5, 0.0, 0.0)], &[(6.0, 0.0, 0.0), (0.0, 0.0, 0.0)]),
),
];
for (name, make, open_top, truth, probes) in cases {
for (pose, place) in poses() {
let mut topo = Topology::new();
let solid = make(&mut topo);
transform_solid(&mut topo, solid, &place).unwrap();
let up = place.mul_point(Point3::new(0.0, 0.0, 1.0))
- place.mul_point(Point3::new(0.0, 0.0, 0.0));
let open = if open_top {
vec![top_face(&topo, solid, up)]
} else {
vec![]
};
let hollow = shell(&mut topo, solid, 1.0, &open).unwrap();
let label = format!("{name} {pose}");
assert_exact(&topo, hollow, truth, &label);
assert_material(&topo, hollow, &place, probes, &label);
}
}
}