use super::*;
use crate::topology::{CoedgeRecord, LoopRecord};
use crate::{
import_step_report, make_box_brep, make_sphere_brep, solid_scale,
tessellate_brep_watertight,
};
use std::path::PathBuf;
#[test]
fn clean_two_rim_torus_uses_exact_cross_band() {
let surface =
crate::make_torus_surface(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 4.0, 1.0).unwrap();
let [u0, u1] = surface.domain_u().unwrap();
let [v0, v1] = surface.domain_v().unwrap();
let v_lo = v0 + 0.2 * (v1 - v0);
let v_hi = v0 + 0.6 * (v1 - v0);
let rim = |id, edge_id, from_u, to_u, v| LoopRecord {
id,
coedges: vec![CoedgeRecord {
id: id + 10,
edge_id,
forward: true,
pcurve: make_line(Vec3::new(from_u, v, 0.0), Vec3::new(to_u, v, 0.0)).unwrap(),
}],
};
let mut face = FaceRecord {
id: 1,
surface,
same_sense: true,
loops: vec![rim(2, 3, u0, u1, v_lo), rim(4, 5, u1, u0, v_hi)],
name: None,
};
let classify = |face: &FaceRecord, v| {
parameter_point_in_face(
face,
Vec2 {
x: u0 + 0.37 * (u1 - u0),
y: v,
},
1e-9,
)
.unwrap()
};
assert_eq!(classify(&face, 0.5 * (v_lo + v_hi)), PolygonClass::Inside);
assert_eq!(classify(&face, v0 + 0.8 * (v1 - v0)), PolygonClass::Outside);
assert_eq!(classify(&face, v_lo), PolygonClass::Boundary);
face.same_sense = false;
assert_eq!(classify(&face, 0.5 * (v_lo + v_hi)), PolygonClass::Outside);
assert_eq!(classify(&face, v0 + 0.8 * (v1 - v0)), PolygonClass::Inside);
}
#[test]
fn abc_11516_clean_torus_rim_centroids_follow_tessellated_band() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../step-validation/data/files/abc_0001_step_v00/00011516/00011516_547fd4f9ecd74f86ba27d0c7_step_032.step",
);
if !path.exists() {
return; }
let text = std::fs::read_to_string(path).unwrap();
let (solids, _failed, _error) = import_step_report(&text).unwrap();
let face = solids
.iter()
.flat_map(|solid| &solid.shells)
.flat_map(|shell| &shell.faces)
.find(|face| face.id == 5339)
.expect("ABC 11516 torus face 5339");
assert_eq!(
face.loops
.iter()
.map(|loop_| loop_.coedges.len())
.collect::<Vec<_>>(),
[1, 1]
);
assert_eq!(face.surface.closed_directions().unwrap(), (true, true));
for (u, v) in [
(0.9778149013315436, 0.8240114046890169),
(0.9883342281343575, 0.9227205236886824),
(0.9571439746899160, 0.9225631230673472),
] {
assert_ne!(
parameter_point_in_face(face, Vec2 { x: u, y: v }, 1e-7).unwrap(),
PolygonClass::Outside,
"tessellated face5339 centroid ({u}, {v}) fell outside its exact torus band",
);
}
}
#[test]
fn collapsed_torus_puncture_keeps_band_classification_consistent() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/step-import/abc_00000039.step");
let text = std::fs::read_to_string(path).unwrap();
let (solids, failed, error) = import_step_report(&text).unwrap();
assert_eq!(failed, 0, "fixture reported failed solids: {error:?}");
assert_eq!(solids.len(), 1);
let solid = &solids[0];
let faces: Vec<_> = solid.shells.iter().flat_map(|shell| &shell.faces).collect();
let targets: Vec<usize> = faces
.iter()
.enumerate()
.filter_map(|(index, face)| {
(face.loops.len() == 3
&& face.surface.closed_directions().ok() == Some((true, true)))
.then_some(index)
})
.collect();
assert_eq!(targets.len(), 2, "expected the two punctured torus bands");
let chord = (solid_scale(solid) * 1.5e-3).max(1e-9);
let mesh = tessellate_brep_watertight(solid, chord).unwrap();
let mut checked = 0usize;
for (triangle_index, triangle) in mesh.indices.chunks_exact(3).enumerate() {
let face_index = mesh.face_ids[triangle_index] as usize;
if !targets.contains(&face_index) {
continue;
}
let point = |index: u32| {
let offset = index as usize * 3;
Vec3::new(
mesh.positions[offset],
mesh.positions[offset + 1],
mesh.positions[offset + 2],
)
};
let centroid = point(triangle[0])
.add(point(triangle[1]))
.add(point(triangle[2]))
.scale(1.0 / 3.0);
let projection =
project_point_to_surface(&faces[face_index].surface, centroid).unwrap();
let class = parameter_point_in_face(
faces[face_index],
Vec2 {
x: projection.u,
y: projection.v,
},
1e-7,
)
.unwrap();
assert_ne!(
class,
PolygonClass::Outside,
"face {} mesh triangle {triangle_index} classified outside its band at ({:.6}, {:.6})",
faces[face_index].id,
projection.u,
projection.v,
);
checked += 1;
}
assert!(
checked > 100,
"expected dense torus-band coverage, checked {checked}"
);
}
#[test]
fn box_edge_point_classifies_on_with_mean_normal() {
let solid = make_box_brep(Vec3::default(), 10.0, 10.0, 10.0).unwrap();
let classification = classify_point(Vec3::new(10.0, 10.0, 5.0), &solid, 1e-7).unwrap();
assert_eq!(classification.class, PointClass::On);
let normal = classification.on_normal.unwrap();
let expected = Vec3::new(1.0, 1.0, 0.0).normalized().unwrap();
assert!(
normal.sub(expected).length() < 1e-6,
"mean normal {normal:?} != {expected:?}"
);
}
#[test]
fn tangency_station_probe_ray_counts_small_blend_tube_crossing() {
use crate::{extrude_profile_brep, make_arc, make_line};
let y_star = 5.0 - 1.8f64.sqrt();
let fillet_arc = make_arc(
Vec3::new(10.4, y_star, 0.0),
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
0.4,
0.0,
(2.0f64 / 7.0).acos(),
)
.unwrap();
let boss_tangency = Vec3::new(
10.4 - 0.4 * (2.0 / 7.0),
y_star + 0.4 * (1.0f64 - (2.0f64 / 7.0).powi(2)).sqrt(),
0.0,
);
let boss_arc = make_arc(
Vec3::new(10.0, 5.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
1.0,
(boss_tangency.y - 5.0).atan2(boss_tangency.x - 10.0),
std::f64::consts::FRAC_PI_2,
)
.unwrap();
let profile = vec![
make_line(Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0)).unwrap(),
make_line(Vec3::new(10.0, 0.0, 0.0), Vec3::new(10.0, y_star, 0.0)).unwrap(),
fillet_arc,
boss_arc,
make_line(Vec3::new(10.0, 6.0, 0.0), Vec3::new(10.0, 10.0, 0.0)).unwrap(),
make_line(Vec3::new(10.0, 10.0, 0.0), Vec3::new(0.0, 10.0, 0.0)).unwrap(),
make_line(Vec3::new(0.0, 10.0, 0.0), Vec3::new(0.0, 0.0, 0.0)).unwrap(),
];
let solid = extrude_profile_brep(&profile, Vec3::new(0.0, 0.0, 1.0), 10.0).unwrap();
assert!(solid.validate().is_empty(), "tangency fixture invalid");
let inside = Vec3::new(9.9312, y_star, 9.898592);
assert_eq!(
classify_point(inside, &solid, 1e-7).unwrap().class,
PointClass::In,
"tangency-station interior probe must classify In"
);
let void = Vec3::new(10.2, y_star, 9.9);
assert_eq!(
classify_point(void, &solid, 1e-7).unwrap().class,
PointClass::Out,
"blend-tube void probe must classify Out"
);
}
#[test]
fn sphere_classifies_inside_outside_and_boundary() {
let sphere = make_sphere_brep(Vec3::default(), 3.0, Vec3::new(0.0, 1.0, 0.0)).unwrap();
assert_eq!(
classify_point(Vec3::default(), &sphere, 1e-7)
.unwrap()
.class,
PointClass::In
);
assert_eq!(
classify_point(Vec3::new(5.0, 0.0, 0.0), &sphere, 1e-7)
.unwrap()
.class,
PointClass::Out
);
assert_eq!(
classify_point(Vec3::new(3.0, 0.0, 0.0), &sphere, 1e-7)
.unwrap()
.class,
PointClass::On
);
}
#[test]
fn seam_straddling_loop_interior_point_classifies_inside() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("boolean_fuzz_corpus")
.join("16_seam_straddle_containment")
.join("a.json");
let solid: BrepSolid =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let face = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == 519)
.expect("face 519");
let query = Vec2 {
x: 0.5800652,
y: 0.6063725,
};
assert_eq!(
parameter_point_in_face(face, query, 1e-7).unwrap(),
PolygonClass::Inside,
"interior point on the seam-straddling face must classify Inside"
);
}
#[test]
fn seam_straddling_hole_interior_point_classifies_outside() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("boolean_fuzz_corpus")
.join("16_seam_straddle_containment")
.join("a.json");
let solid: BrepSolid =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let face = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == 519)
.expect("face 519");
assert_eq!(
parameter_point_in_face(
face,
Vec2 {
x: 0.986264598,
y: 0.735901351,
},
1e-7
)
.unwrap(),
PolygonClass::Outside,
"point inside the seam-straddling hole must classify Outside"
);
assert_eq!(
parameter_point_in_face(
face,
Vec2 {
x: 0.986264598,
y: 0.4,
},
1e-7
)
.unwrap(),
PolygonClass::Inside,
"material at the same azimuth below the hole must stay Inside"
);
}
#[test]
fn doubly_periodic_near_rim_point_belongs_to_neighbor_face() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("boolean_fuzz_corpus")
.join("17_doubly_periodic_seam_image")
.join("a.json");
let solid: BrepSolid =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let face_27 = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == 27)
.expect("face 27");
let face_40 = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == 40)
.expect("face 40");
assert_eq!(
parameter_point_in_face(
face_27,
Vec2 {
x: 0.4311420,
y: 0.9989635,
},
1e-7
)
.unwrap(),
PolygonClass::Outside,
"near-rim point outside face 27's v-band [0,0.25] must stay Outside"
);
assert_eq!(
parameter_point_in_face(
face_40,
Vec2 {
x: 0.9311420,
y: 0.9996362,
},
1e-7
)
.unwrap(),
PolygonClass::Inside,
"the same 3D point must classify Inside its true owner, face 40"
);
assert_eq!(
parameter_point_in_face(face_27, Vec2 { x: 0.5, y: 0.125 }, 1e-7).unwrap(),
PolygonClass::Inside,
"mid-band point must classify Inside face 27"
);
}
#[test]
fn ray_parity_thin_flange_point_outside_material_classifies_out() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("boolean_fuzz_corpus")
.join("18_ray_parity_thin_flange")
.join("a.json");
let solid: BrepSolid =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let point = Vec3::new(-50.8272, 437.2548, 0.0);
assert_eq!(
classify_point(point, &solid, 1e-7).unwrap().class,
PointClass::Out,
"a point 2 mm outside the material must classify Out"
);
assert_eq!(
classify_point(Vec3::new(5000.0, 5000.0, 5000.0), &solid, 1e-7)
.unwrap()
.class,
PointClass::Out
);
}
#[test]
fn single_loop_seam_straddler_strip_orientation() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("boolean_fuzz_corpus")
.join("18_ray_parity_thin_flange")
.join("a.json");
let solid: BrepSolid =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let face = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == 675)
.expect("face 675");
assert_eq!(
parameter_point_in_face(
face,
Vec2 {
x: 0.0027,
y: 0.505,
},
1e-7
)
.unwrap(),
PolygonClass::Inside,
"the seam-strip interior must classify Inside"
);
for (u, v) in [(0.318, 0.7), (0.9, 0.5)] {
assert_eq!(
parameter_point_in_face(face, Vec2 { x: u, y: v }, 1e-7).unwrap(),
PolygonClass::Outside,
"far azimuth ({u},{v}) must classify Outside"
);
}
}
#[test]
fn band_rim_containment_scalloped_and_covering_plane_faces() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("boolean_fuzz_corpus")
.join("21_band_rim_containment")
.join("a.json");
let solid: BrepSolid =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let face = |id: u64| {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == id)
.unwrap_or_else(|| panic!("face {id}"))
};
let f1604 = face(1604);
for (u, v) in [(0.5, 0.625), (0.001, 0.625), (0.999, 0.625)] {
assert_eq!(
parameter_point_in_face(f1604, Vec2 { x: u, y: v }, 1e-7).unwrap(),
PolygonClass::Inside,
"face 1604 band interior ({u},{v}) must classify Inside"
);
}
for (u, v) in [(0.33, 0.70), (0.5, 0.3), (0.5, 0.9)] {
assert_eq!(
parameter_point_in_face(f1604, Vec2 { x: u, y: v }, 1e-7).unwrap(),
PolygonClass::Outside,
"face 1604 non-material ({u},{v}) must classify Outside \
(0.33,0.70 is the scallop dip zone the old hack force-filled)"
);
}
let f1616 = face(1616);
assert_eq!(
parameter_point_in_face(f1616, Vec2 { x: 0.05, y: 0.625 }, 1e-7).unwrap(),
PolygonClass::Inside,
"face 1616 band interior must classify Inside"
);
assert_eq!(
parameter_point_in_face(f1616, Vec2 { x: 0.165, y: 0.70 }, 1e-7).unwrap(),
PolygonClass::Outside,
"face 1616 scallop dip zone must classify Outside"
);
let f1513 = face(1513);
for (u, v) in [(0.524, 0.503), (0.216, 0.5), (0.3, 0.5), (0.5, 0.999)] {
assert_eq!(
parameter_point_in_face(f1513, Vec2 { x: u, y: v }, 1e-7).unwrap(),
PolygonClass::Inside,
"face 1513 strip interior ({u},{v}) must classify Inside"
);
}
for (u, v) in [(0.216, 0.9), (0.9, 0.99)] {
assert_eq!(
parameter_point_in_face(f1513, Vec2 { x: u, y: v }, 1e-7).unwrap(),
PolygonClass::Outside,
"face 1513 above the scalloped rim dip ({u},{v}) must classify Outside"
);
}
}