use super::*;
use crate::{
boolean_operation, fillet_edges, make_arc, make_box_brep, make_cone_brep,
make_cylinder_brep, make_line, make_sphere_brep, revolve_profile_brep,
solid_mass_properties, BooleanOperation, BooleanOptions,
};
fn extreme_face_id(solid: &BrepSolid, axis: Vec3) -> u64 {
let mut best = f64::NEG_INFINITY;
let mut id = 0;
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
if let Ok(point) = face.surface.evaluate(0.5, 0.5) {
let value = point.dot(axis);
if value > best {
best = value;
id = face.id;
}
}
}
id
}
fn one_use_edge_count(solid: &BrepSolid) -> usize {
let mut counts = HashMap::<u64, usize>::default();
for coedge in solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
{
*counts.entry(coedge.edge_id).or_default() += 1;
}
counts.values().filter(|count| **count == 1).count()
}
fn open_real_edge_count(solid: &BrepSolid) -> usize {
let mut counts = HashMap::<u64, usize>::default();
for coedge in solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
{
*counts.entry(coedge.edge_id).or_default() += 1;
}
solid
.edges
.iter()
.filter(|edge| !edge.degenerate && counts.get(&edge.id).copied().unwrap_or(0) == 1)
.count()
}
fn spherical_cap_solid(radius: f64, cut_z: f64) -> BrepSolid {
let rim_radius = (radius * radius - cut_z * cut_z).sqrt();
let start_angle = cut_z.atan2(rim_radius);
let dome = make_arc(
Vec3::default(),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
radius,
start_angle,
std::f64::consts::FRAC_PI_2,
)
.unwrap();
let disk = make_line(
Vec3::new(0.0, 0.0, cut_z),
Vec3::new(rim_radius, 0.0, cut_z),
)
.unwrap();
let axis_line = make_line(Vec3::new(0.0, 0.0, radius), Vec3::new(0.0, 0.0, cut_z)).unwrap();
revolve_profile_brep(
&[disk, dome, axis_line],
Vec3::default(),
Vec3::new(0.0, 0.0, 1.0),
std::f64::consts::TAU,
)
.unwrap()
}
fn hemisphere_cup(radius: f64, height: f64) -> BrepSolid {
let top = make_line(Vec3::new(0.0, 0.0, height), Vec3::new(radius, 0.0, height)).unwrap();
let side = make_line(Vec3::new(radius, 0.0, height), Vec3::new(radius, 0.0, 0.0)).unwrap();
let dome = make_arc(
Vec3::default(),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, -1.0),
radius,
0.0,
std::f64::consts::FRAC_PI_2,
)
.unwrap();
let axis_line =
make_line(Vec3::new(0.0, 0.0, -radius), Vec3::new(0.0, 0.0, height)).unwrap();
revolve_profile_brep(
&[top, side, dome, axis_line],
Vec3::default(),
Vec3::new(0.0, 0.0, 1.0),
std::f64::consts::TAU,
)
.unwrap()
}
#[test]
fn cylinder_open_top_shell_is_watertight_with_exact_wall_volume() {
let axis = Vec3::new(0.0, 0.0, 1.0);
let cylinder = make_cylinder_brep(Vec3::default(), axis, 2.0, 4.0).unwrap();
let top = extreme_face_id(&cylinder, axis);
let result = offset_shell(&cylinder, &[top], 0.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(one_use_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
assert!(result.face_images.iter().all(|image| cylinder
.shells
.iter()
.flat_map(|shell| &shell.faces)
.any(|face| face.id == image.source_face_id)));
let outer = std::f64::consts::PI * 2.0_f64.powi(2) * 4.0;
let inner = std::f64::consts::PI * 1.5_f64.powi(2) * 3.5;
let expected = outer - inner;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!((volume - expected).abs() < 1e-3, "{volume} vs {expected}");
let outcome = offset_shell_with_diagnostics(&cylinder, &[top], 0.5, None).unwrap();
assert!(outcome.diagnostics.shippable());
assert_eq!(outcome.diagnostics.counters["validate.issues"], 0);
}
#[test]
fn cone_open_top_shell_is_watertight_with_exact_wall_volume() {
let axis = Vec3::new(0.0, 0.0, 1.0);
let cone = make_cone_brep(Vec3::default(), axis, 2.0, 1.0, 4.0).unwrap();
let top = extreme_face_id(&cone, axis);
let result = offset_shell(&cone, &[top], 0.4).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(one_use_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
let root17 = 17.0_f64.sqrt();
let (base_r, base_z) = (2.0 - 1.6 / root17, -0.4 / root17);
let top_r = 1.0 - 1.6 / root17;
let top_z = 4.0 - 0.4 / root17;
let cap_r = base_r - (0.4 - base_z) / 4.0; let pi = std::f64::consts::PI;
let frustum = |r0: f64, r1: f64, h: f64| pi * h / 3.0 * (r0 * r0 + r0 * r1 + r1 * r1);
let source = frustum(2.0, 1.0, 4.0);
let cavity = frustum(cap_r, top_r, top_z - 0.4);
let cork = frustum(top_r, 1.0, 4.0 - top_z);
let expected = source - cavity - cork;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!((volume - expected).abs() < 2e-3, "{volume} vs {expected}");
let outcome = offset_shell_with_diagnostics(&cone, &[top], 0.4, None).unwrap();
assert!(outcome.diagnostics.shippable());
assert_eq!(outcome.diagnostics.counters["validate.issues"], 0);
}
#[test]
fn open_top_box_offsets_to_exact_hollow_volume() {
let source = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
let top = source.shells[0]
.faces
.iter()
.find(|face| {
let point = face.surface.evaluate(0.5, 0.5).unwrap();
(point.z - 4.0).abs() < 1e-9
})
.unwrap();
let result = offset_shell(&source, &[top.id], 0.5).unwrap();
assert!(result.solid.validate().is_empty());
assert!(result.face_images.iter().all(|image| source
.shells
.iter()
.flat_map(|shell| &shell.faces)
.any(|face| face.id == image.source_face_id)));
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!((volume - 32.5).abs() < 1e-5, "{volume}");
}
#[test]
fn sphere_opening_shell_is_watertight_with_exact_sector_volume() {
let radius = 2.0;
let cut_z = 1.0;
let distance = 0.3;
let solid = spherical_cap_solid(radius, cut_z);
let disk = extreme_face_id(&solid, Vec3::new(0.0, 0.0, -1.0));
let result = offset_shell(&solid, &[disk], distance).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
assert!(result.face_images.iter().all(|image| solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.any(|face| face.id == image.source_face_id)));
let expected = 2.0 * std::f64::consts::PI / 3.0
* (1.0 - cut_z / radius)
* (radius.powi(3) - (radius - distance).powi(3));
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-6,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
let outcome = offset_shell_with_diagnostics(&solid, &[disk], distance, None).unwrap();
assert!(outcome.diagnostics.shippable());
assert_eq!(outcome.diagnostics.counters["validate.issues"], 0);
}
#[test]
fn hemisphere_cup_shell_is_watertight() {
let solid = hemisphere_cup(2.0, 3.0);
let top = extreme_face_id(&solid, Vec3::new(0.0, 0.0, 1.0));
let result = offset_shell(&solid, &[top], 0.4).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
let pi = std::f64::consts::PI;
let expected = pi
* ((4.0 * 3.0 + 2.0 / 3.0 * 8.0) - (1.6f64.powi(2) * 3.0 + 2.0 / 3.0 * 1.6f64.powi(3)));
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 5e-3,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
}
#[test]
fn tube_open_both_ends_shells_to_exact_annulus_walls() {
let axis = Vec3::new(0.0, 0.0, 1.0);
let cylinder = make_cylinder_brep(Vec3::default(), axis, 2.0, 4.0).unwrap();
let top = extreme_face_id(&cylinder, axis);
let bottom = extreme_face_id(&cylinder, axis.scale(-1.0));
let result = offset_shell(&cylinder, &[top, bottom], 0.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(one_use_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
let expected = std::f64::consts::PI * (2.0f64.powi(2) - 1.5f64.powi(2)) * 4.0;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-6,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
let outcome = offset_shell_with_diagnostics(&cylinder, &[top, bottom], 0.5, None).unwrap();
assert!(outcome.diagnostics.shippable());
assert_eq!(outcome.diagnostics.counters["validate.issues"], 0);
}
#[test]
fn frustum_open_both_ends_shells_watertight_with_exact_wall_volume() {
let axis = Vec3::new(0.0, 0.0, 1.0);
let cone = make_cone_brep(Vec3::default(), axis, 2.0, 1.0, 4.0).unwrap();
let top = extreme_face_id(&cone, axis);
let bottom = extreme_face_id(&cone, axis.scale(-1.0));
let distance = 0.3;
let result = offset_shell(&cone, &[top, bottom], distance).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(one_use_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
let root17 = 17.0f64.sqrt();
let delta = distance * root17 / 4.0;
let tau = distance / root17;
let z1 = 4.0 - tau;
let pi = std::f64::consts::PI;
let first = pi * delta * ((4.0 - delta) * z1 - z1 * z1 / 4.0);
let second = pi * (4.25 * tau * tau - 5.3125 * tau * tau * tau);
let expected = first + second;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-4,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
}
#[test]
fn box_through_hole_open_both_ends_shells_to_two_wall_tubes() {
let box_solid = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
let drill = make_cylinder_brep(
Vec3::new(2.0, 2.0, -1.0),
Vec3::new(0.0, 0.0, 1.0),
1.0,
6.0,
)
.unwrap();
let solid = boolean_operation(
&box_solid,
&drill,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let top = extreme_face_id(&solid, Vec3::new(0.0, 0.0, 1.0));
let bottom = extreme_face_id(&solid, Vec3::new(0.0, 0.0, -1.0));
let result = offset_shell(&solid, &[top, bottom], 0.3).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(one_use_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 2, "outer tube + hole tube");
let pi = std::f64::consts::PI;
let expected = (16.0 - 3.4f64 * 3.4) * 4.0 + pi * (1.3f64.powi(2) - 1.0) * 4.0;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-5,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
}
#[test]
fn oblique_cylinder_through_hole_shells_watertight_with_exact_wall_volume() {
let tilt = 30f64.to_radians();
let pi = std::f64::consts::PI;
let expected =
(60.0 - 9.5f64 * 5.5) * 6.0 + pi * (0.95f64.powi(2) - 0.49) * 6.0 / tilt.cos();
let mut witnessed = Vec::new();
for _ in 0..3 {
let box_solid = make_box_brep(Vec3::default(), 10.0, 6.0, 6.0).unwrap();
let axis = Vec3::new(tilt.sin(), 0.0, tilt.cos());
let drill = make_cylinder_brep(
Vec3::new(5.0 - 4.5 * axis.x, 3.0, 3.0 - 4.5 * axis.z),
axis,
0.7,
9.0,
)
.unwrap();
let solid = boolean_operation(
&box_solid,
&drill,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let top = extreme_face_id(&solid, Vec3::new(0.0, 0.0, 1.0));
let bottom = extreme_face_id(&solid, Vec3::new(0.0, 0.0, -1.0));
let result = offset_shell(&solid, &[top, bottom], 0.25).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(one_use_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 2, "outer box + oblique tube");
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-5,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
let faces = result
.solid
.shells
.iter()
.map(|shell| shell.faces.len())
.sum::<usize>();
witnessed.push((faces, volume));
}
for later in &witnessed[1..] {
assert_eq!(witnessed[0].0, later.0, "run-to-run face-count drift");
assert!(
(witnessed[0].1 - later.1).abs() < 1e-9,
"run-to-run volume drift: {} vs {}",
witnessed[0].1,
later.1
);
}
}
#[test]
fn conical_through_hole_shells_watertight_with_exact_wall_volume() {
let pi = std::f64::consts::PI;
let s = -1.0f64 / 12.0;
let delta = 0.3 * (1.0 + s * s).sqrt();
let expected = (16.0 - 3.4f64 * 3.4) * 4.0 + pi * delta * (7.6 + 4.0 * delta);
let mut witnessed = Vec::new();
for _ in 0..3 {
let box_solid = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
let drill = make_cone_brep(
Vec3::new(2.0, 2.0, -1.0),
Vec3::new(0.0, 0.0, 1.0),
1.2,
0.7,
6.0,
)
.unwrap();
let solid = boolean_operation(
&box_solid,
&drill,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let top = extreme_face_id(&solid, Vec3::new(0.0, 0.0, 1.0));
let bottom = extreme_face_id(&solid, Vec3::new(0.0, 0.0, -1.0));
let result = offset_shell(&solid, &[top, bottom], 0.3).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(one_use_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 2, "outer box + tapered tube");
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-5,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
let faces = result
.solid
.shells
.iter()
.map(|shell| shell.faces.len())
.sum::<usize>();
witnessed.push((faces, volume));
}
for later in &witnessed[1..] {
assert_eq!(witnessed[0].0, later.0, "run-to-run face-count drift");
assert!(
(witnessed[0].1 - later.1).abs() < 1e-9,
"run-to-run volume drift: {} vs {}",
witnessed[0].1,
later.1
);
}
}
#[test]
fn oblique_through_hole_rim_pair_still_refuses() {
let box_solid = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
let drill = make_cone_brep(
Vec3::new(2.0, 2.0, -1.0),
Vec3::new(0.0, 0.0, 1.0),
1.6,
0.6,
6.0,
)
.unwrap();
let solid = boolean_operation(
&box_solid,
&drill,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let top = extreme_face_id(&solid, Vec3::new(0.0, 0.0, 1.0));
let bottom = extreme_face_id(&solid, Vec3::new(0.0, 0.0, -1.0));
let error = offset_shell(&solid, &[top, bottom], 0.3).unwrap_err();
assert!(
error.contains("unwelded rim leaves a non-watertight shell"),
"{error}"
);
}
#[test]
fn sphere_carved_opening_shell_is_watertight() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let sphere =
make_sphere_brep(Vec3::new(10.0, 15.0, 10.0), 8.0, Vec3::new(0.0, 0.0, 1.0)).unwrap();
let carved = boolean_operation(
&box_solid,
&sphere,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let opening = extreme_face_id(&carved, Vec3::new(0.0, 1.0, 0.0));
let result = offset_shell(&carved, &[opening], 1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
assert!(
result
.face_images
.iter()
.any(|image| matches!(image.role, OffsetFaceRole::Offset)
&& !carved
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == image.source_face_id)
.and_then(|face| face.surface.is_affine().ok())
.unwrap_or(true)),
"the curved (sphere) cavity wall must appear as an offset image"
);
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
volume > 0.0 && volume < 8000.0,
"hollow shell wall volume implausible: {volume}"
);
}
#[test]
fn side_arc_carved_opening_shells_watertight() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let side = make_cylinder_brep(
Vec3::new(-5.0, 20.0, 10.0),
Vec3::new(1.0, 0.0, 0.0),
6.0,
30.0,
)
.unwrap();
let solid = boolean_operation(
&box_solid,
&side,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let opening = extreme_face_id(&solid, Vec3::new(0.0, 1.0, 0.0));
let result = offset_shell(&solid, &[opening], 1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
volume > 0.0 && volume < 8000.0,
"hollow shell wall volume implausible: {volume}"
);
}
#[test]
fn cone_crater_opening_shells_watertight_with_exact_wall_volume() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let crater = make_cone_brep(
Vec3::new(10.0, 24.0, 10.0),
Vec3::new(0.0, -1.0, 0.0),
9.0,
0.0,
12.0,
)
.unwrap();
let carved = boolean_operation(
&box_solid,
&crater,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let opening = carved
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| {
face.surface.is_affine().unwrap_or(false)
&& (face.surface.evaluate(0.5, 0.5).unwrap().y - 20.0).abs() < 1e-9
})
.unwrap()
.id;
let result = offset_shell(&carved, &[opening], 1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(
result.solid.shells.len(),
2,
"box walls + detached crater funnel"
);
assert!(result.face_images.iter().all(|image| carved
.shells
.iter()
.flat_map(|shell| &shell.faces)
.any(|face| face.id == image.source_face_id)));
let pi = std::f64::consts::PI;
let source = 8000.0 - 96.0 * pi;
let dilated_crater = pi
* (0.5625 * (10.5f64.powi(3) - 1.6f64.powi(3)) / 3.0
+ (2.25 * 0.6 - (1.5f64.powi(3) - 0.9f64.powi(3)) / 3.0));
let cavity = 17.0 * 18.5 * 17.0 - dilated_crater;
let expected = source - cavity;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 5e-3,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
}
#[test]
fn apex_cone_open_base_shells_watertight_with_exact_wall_volume() {
let radius = 8.0f64;
let height = 12.0f64;
let distance = 1.0f64;
let cone = make_cone_brep(
Vec3::default(),
Vec3::new(0.0, 0.0, 1.0),
radius,
0.0,
height,
)
.unwrap();
let base = extreme_face_id(&cone, Vec3::new(0.0, 0.0, -1.0));
let result = offset_shell(&cone, &[base], distance).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
let slant = (radius * radius + height * height).sqrt();
let sin_alpha = radius / slant;
let cavity_apex_z = height - distance / sin_alpha;
let cavity_base_radius = {
let base_offset_r = radius - distance * height / slant;
let base_offset_z = -distance * radius / slant;
base_offset_r + base_offset_z * (base_offset_r / (cavity_apex_z - base_offset_z))
};
let pi = std::f64::consts::PI;
let expected = pi * radius * radius * height / 3.0
- pi * cavity_base_radius * cavity_base_radius * cavity_apex_z / 3.0;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-3,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
}
#[test]
fn outward_shell_open_top_box_has_exact_grown_wall_volume() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let opening = extreme_face_id(&box_solid, Vec3::new(0.0, 1.0, 0.0));
let result = offset_shell(&box_solid, &[opening], -1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
let expected = 23.0 * 21.5 * 23.0 - 8000.0;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-6,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
}
#[test]
fn outward_shell_conic_crater_carved_opening_has_exact_wall_volume() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let cone = make_cone_brep(
Vec3::new(10.0, 24.0, 10.0),
Vec3::new(0.0, -1.0, 0.0),
9.0,
0.0,
12.0,
)
.unwrap();
let solid = boolean_operation(
&box_solid,
&cone,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let opening = extreme_face_id(&solid, Vec3::new(0.0, 1.0, 0.0));
let result = offset_shell(&solid, &[opening], -1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(
result.solid.shells.len(),
2,
"grown box shell + crater funnel"
);
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
let pi = std::f64::consts::PI;
let grown = 23.0 * 21.5 * 23.0 - pi * 4.125f64.powi(2) * 5.5 / 3.0;
let source = 8000.0 - pi * 36.0 * 8.0 / 3.0;
let expected = grown - source;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 1e-3,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
}
#[test]
fn outward_shell_sphere_carved_opening_shells_watertight() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let sphere =
make_sphere_brep(Vec3::new(10.0, 15.0, 10.0), 8.0, Vec3::new(0.0, 0.0, 1.0)).unwrap();
let solid = boolean_operation(
&box_solid,
&sphere,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let opening = extreme_face_id(&solid, Vec3::new(0.0, 1.0, 0.0));
let result = offset_shell(&solid, &[opening], -1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(
result.solid.shells.len(),
2,
"grown box shell + gouge dome"
);
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
let pi = std::f64::consts::PI;
let below =
|r: f64, h: f64| 4.0 / 3.0 * pi * r.powi(3) - pi * h * h * (3.0 * r - h) / 3.0;
let grown = 23.0 * 21.5 * 23.0 - below(6.5, 1.5);
let source = 8000.0 - below(8.0, 3.0);
let expected = grown - source;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
(volume - expected).abs() < 0.1,
"{volume} vs {expected} (diff {:.3e})",
volume - expected
);
let box_component = result
.solid
.shells
.iter()
.map(|shell| {
let single = BrepSolid {
id: result.solid.id,
vertices: result.solid.vertices.clone(),
edges: result.solid.edges.clone(),
shells: vec![shell.clone()],
genus: 0,
};
solid_mass_properties(&single).unwrap().volume
})
.fold(0.0f64, f64::max);
assert!(
(box_component - 3373.5).abs() < 1e-6,
"grown box component should be exact: {box_component}"
);
}
#[test]
fn outward_shell_bore_through_opened_face_still_refuses() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let drill = make_cylinder_brep(
Vec3::new(10.0, -1.0, 10.0),
Vec3::new(0.0, 1.0, 0.0),
4.0,
22.0,
)
.unwrap();
let solid = boolean_operation(
&box_solid,
&drill,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let opening = extreme_face_id(&solid, Vec3::new(0.0, 1.0, 0.0));
assert!(
offset_shell(&solid, &[opening], -1.5).is_err(),
"outward through-bore is unsupported and must refuse, not emit a wrong shell"
);
}
#[test]
fn concave_pocket_solid_shells_watertight_through_uncarved_face() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let pocket = make_box_brep(Vec3::new(15.0, 5.0, 5.0), 6.0, 10.0, 10.0).unwrap();
let solid = boolean_operation(
&box_solid,
&pocket,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let opening = extreme_face_id(&solid, Vec3::new(0.0, 1.0, 0.0));
let result = offset_shell(&solid, &[opening], 1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
volume > 0.0 && volume < 8000.0,
"hollow shell wall volume implausible: {volume}"
);
}
#[test]
fn l_step_solid_shells_watertight_through_carved_face() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let step = make_box_brep(Vec3::new(10.0, 10.0, -1.0), 11.0, 11.0, 22.0).unwrap();
let solid = boolean_operation(
&box_solid,
&step,
BooleanOperation::Subtract,
&BooleanOptions::default(),
)
.unwrap();
let opening = extreme_face_id(&solid, Vec3::new(0.0, 1.0, 0.0));
let result = offset_shell(&solid, &[opening], 1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(open_real_edge_count(&result.solid), 0, "no open rim edges");
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!(
volume > 0.0 && volume < 8000.0,
"hollow shell wall volume implausible: {volume}"
);
}
#[test]
fn vertical_edge_fillet_at_opening_corner_shells_to_exact_hollow_volume() {
let box_solid = make_box_brep(Vec3::default(), 20.0, 20.0, 20.0).unwrap();
let solid = fillet_edges(
&box_solid,
&[Vec3::new(20.0, 20.0, 10.0)],
None,
3.0,
false,
Some("F"),
)
.unwrap();
assert!(solid.validate().is_empty(), "{:?}", solid.validate());
let top = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| face.surface.is_affine().unwrap_or(false))
.max_by(|first, second| {
let first_z = first.surface.evaluate(0.5, 0.5).unwrap().z;
let second_z = second.surface.evaluate(0.5, 0.5).unwrap().z;
first_z.total_cmp(&second_z)
})
.unwrap()
.id;
let result = offset_shell(&solid, &[top], 1.5).unwrap();
assert!(
result.solid.validate().is_empty(),
"{:?}",
result.solid.validate()
);
assert_eq!(one_use_edge_count(&result.solid), 0, "no open rim edges");
assert_eq!(result.solid.shells.len(), 1, "one connected shell");
let cove = |radius: f64| radius * radius * (1.0 - std::f64::consts::PI / 4.0);
let fixture = 8000.0 - cove(3.0) * 20.0;
let cavity = (17.0 * 17.0 - cove(1.5)) * 18.5;
let expected = fixture - cavity;
let volume = solid_mass_properties(&result.solid).unwrap().volume;
assert!((volume - expected).abs() < 2e-2, "{volume} vs {expected}");
}
#[test]
fn sphere_as_opening_face_still_refuses() {
let solid = hemisphere_cup(2.0, 3.0);
let dome = extreme_face_id(&solid, Vec3::new(0.0, 0.0, -1.0));
assert!(offset_shell(&solid, &[dome], 0.4).is_err());
}
#[test]
fn diagnostic_offset_shell_reports_validation_metrics() {
let source = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
let top = source.shells[0]
.faces
.iter()
.find(|face| {
let point = face.surface.evaluate(0.5, 0.5).unwrap();
(point.z - 4.0).abs() < 1e-9
})
.unwrap();
let outcome = offset_shell_with_diagnostics(&source, &[top.id], 0.5, None).unwrap();
assert!(outcome.diagnostics.shippable());
assert_eq!(outcome.diagnostics.counters["collect.opening_faces"], 1);
assert_eq!(outcome.diagnostics.counters["validate.issues"], 0);
assert!(outcome
.diagnostics
.measurements
.contains_key("validate.max_pcurve_error"));
}