use super::*;
use crate::ClippingProcessor;
fn push_quad(m: &mut Mesh, quad: [Point3<f64>; 4], target: Vector3<f64>) {
let n = (quad[1] - quad[0])
.cross(&(quad[2] - quad[0]))
.try_normalize(1e-12)
.expect("degenerate quad in fixture");
let q = if n.dot(&target) > 0.0 {
[quad[0], quad[1], quad[2], quad[3]]
} else {
[quad[3], quad[2], quad[1], quad[0]]
};
let nrm = target.normalize();
let b = m.vertex_count() as u32;
for p in &q {
m.add_vertex(*p, nrm);
}
m.add_triangle(b, b + 1, b + 2);
m.add_triangle(b, b + 2, b + 3);
}
fn pre_cut_wall() -> Mesh {
pre_cut_wall_with_slot(2.0)
}
fn pre_cut_wall_with_slot(w: f64) -> Mesh {
let p = |x: f64, y: f64, z: f64| Point3::new(x, y, z);
let (a, b) = (3.0 - w * 0.5, 3.0 + w * 0.5);
let mut m = Mesh::with_capacity(96, 144);
for (y, out) in [(0.0, Vector3::new(0.0, -1.0, 0.0)), (0.4, Vector3::new(0.0, 1.0, 0.0))] {
push_quad(&mut m, [p(0.0, y, 0.0), p(6.0, y, 0.0), p(6.0, y, 0.5), p(0.0, y, 0.5)], out);
push_quad(&mut m, [p(0.0, y, 2.5), p(6.0, y, 2.5), p(6.0, y, 3.0), p(0.0, y, 3.0)], out);
push_quad(&mut m, [p(0.0, y, 0.5), p(a, y, 0.5), p(a, y, 2.5), p(0.0, y, 2.5)], out);
push_quad(&mut m, [p(b, y, 0.5), p(6.0, y, 0.5), p(6.0, y, 2.5), p(b, y, 2.5)], out);
}
push_quad(&mut m, [p(0.0, 0.0, 0.0), p(6.0, 0.0, 0.0), p(6.0, 0.4, 0.0), p(0.0, 0.4, 0.0)], Vector3::new(0.0, 0.0, -1.0));
push_quad(&mut m, [p(0.0, 0.0, 3.0), p(6.0, 0.0, 3.0), p(6.0, 0.4, 3.0), p(0.0, 0.4, 3.0)], Vector3::new(0.0, 0.0, 1.0));
push_quad(&mut m, [p(0.0, 0.0, 0.0), p(0.0, 0.4, 0.0), p(0.0, 0.4, 3.0), p(0.0, 0.0, 3.0)], Vector3::new(-1.0, 0.0, 0.0));
push_quad(&mut m, [p(6.0, 0.0, 0.0), p(6.0, 0.4, 0.0), p(6.0, 0.4, 3.0), p(6.0, 0.0, 3.0)], Vector3::new(1.0, 0.0, 0.0));
push_quad(&mut m, [p(a, 0.0, 0.5), p(a, 0.4, 0.5), p(a, 0.4, 2.5), p(a, 0.0, 2.5)], Vector3::new(1.0, 0.0, 0.0));
push_quad(&mut m, [p(b, 0.0, 0.5), p(b, 0.4, 0.5), p(b, 0.4, 2.5), p(b, 0.0, 2.5)], Vector3::new(-1.0, 0.0, 0.0));
push_quad(&mut m, [p(a, 0.0, 0.5), p(b, 0.0, 0.5), p(b, 0.4, 0.5), p(a, 0.4, 0.5)], Vector3::new(0.0, 0.0, 1.0));
push_quad(&mut m, [p(a, 0.0, 2.5), p(b, 0.0, 2.5), p(b, 0.4, 2.5), p(a, 0.4, 2.5)], Vector3::new(0.0, 0.0, -1.0));
let want = 7.2 - 0.8 * w;
let vol = mesh_signed_volume(&m);
assert!(
(vol - want).abs() < 1e-6,
"pre_cut_wall_with_slot({w}) is malformed or inward-wound: \
signed volume {vol:.6}, expected {want:+.6}"
);
m
}
#[test]
fn a_pull_in_never_inverts_a_shallow_cutter() {
for span in [0.002_f64, 0.004] {
let host = pre_cut_wall_with_slot(span);
let cutter = GeometryRouter::make_box_mesh(
Point3::new(3.0 - span * 0.5, -10.0, 0.5),
Point3::new(3.0 + span * 0.5, 10.4, 2.5),
);
let ext = GeometryRouter::extend_opening_mesh_through_host(
&cutter,
&host,
Vector3::new(1.0, 0.0, 0.0),
);
let (mn, mx) = ext.bounds();
let remaining = (mx.x as f64) - (mn.x as f64);
let (want_lo, want_hi) = (3.0 - span * 0.25, 3.0 + span * 0.25);
assert!(
((mn.x as f64) - want_lo).abs() <= 1.0e-6
&& ((mx.x as f64) - want_hi).abs() <= 1.0e-6,
"span {span}: caps at {:.9} .. {:.9}, expected {want_lo:.9} .. \
{want_hi:.9} (width {remaining:.9})",
mn.x,
mx.x
);
let want_vol = span * 0.5 * 20.4 * 2.0;
let vol = mesh_signed_volume(&ext);
assert!(
(vol - want_vol).abs() <= want_vol * 1.0e-3,
"span {span}: cutter volume {vol:.9}, expected {want_vol:.9}; a \
NEGATIVE value means the caps swapped and the box is inside out"
);
}
}
#[test]
fn remote_coplanar_facet_does_not_outvote_a_local_exit_cap() {
let slab = GeometryRouter::make_box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 1.0, 0.4));
let plate = GeometryRouter::make_box_mesh(Point3::new(5.0, 0.0, 0.4), Point3::new(15.0, 10.0, 0.6));
let mut host = slab.clone();
host.merge(&plate);
let (inner, outer) = (-0.5, 0.4);
let cutter = GeometryRouter::make_box_mesh(Point3::new(0.5, 0.25, inner), Point3::new(1.5, 0.75, outer));
let span = outer - inner;
let extended =
GeometryRouter::extend_opening_mesh_through_host(&cutter, &host, Vector3::new(0.0, 0.0, 1.0));
let clearance = extended.bounds().1.z as f64 - outer;
assert!(
clearance > 0.1 * span,
"a remote coplanar facet must not outvote the local exit cap; the cap was \
pushed clear by only {clearance:.4} of a {span:.4} span"
);
}
#[test]
fn flush_cap_is_not_pushed_into_a_pre_cut_jamb() {
let host = pre_cut_wall();
let cutter = GeometryRouter::make_box_mesh(Point3::new(2.0, -10.0, 0.5), Point3::new(4.0, 10.4, 2.5));
let dir = Vector3::new(1.0, 0.0, 0.0);
let extended = GeometryRouter::extend_opening_mesh_through_host(&cutter, &host, dir);
let clipper = ClippingProcessor::new();
let before = mesh_signed_volume(&host).abs();
let cut = clipper
.subtract_mesh(&host, &extended)
.expect("subtract must not error on two closed boxes");
let removed = before - mesh_signed_volume(&cut).abs();
assert!(
removed.abs() < 1.0e-3,
"a cutter that exactly fills a hole the host already carries must remove \
nothing; removed {removed:.4} m3 (the flush-cap pad ate the piers)"
);
}
#[test]
fn flush_cap_on_a_genuine_exit_is_still_pushed_clear() {
let slab = GeometryRouter::make_box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 1.0, 0.4));
let (inner, outer) = (0.15, 0.4);
let pocket = GeometryRouter::make_box_mesh(Point3::new(0.5, 0.25, inner), Point3::new(1.5, 0.75, outer));
let span = outer - inner;
let extended =
GeometryRouter::extend_opening_mesh_through_host(&pocket, &slab, Vector3::new(0.0, 0.0, 1.0));
let (mn, mx) = extended.bounds();
let (new_inner, new_outer) = (mn.z as f64, mx.z as f64);
let clearance = new_outer - outer;
assert!(
clearance > 0.1 * span && clearance < 1.0 * span,
"an exit cap must be pushed clear of the surface by an opening-relative \
margin; clearance {clearance:.4} is outside (0.1, 1.0) x span {span:.4}"
);
assert!(
(new_inner - inner).abs() < 1.0e-6,
"the floating inner cap must not move, or the pocket becomes a through-hole; \
moved from {inner:.4} to {new_inner:.4}"
);
}
#[test]
fn remove_internal_membrane_no_panic_on_non_finite_coords() {
let mut m = Mesh::new();
for t in 0..4u32 {
let base = t * 3;
for k in 0..3u32 {
m.positions
.extend_from_slice(&[f32::INFINITY, t as f32 + k as f32, k as f32]);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
let out =
GeometryRouter::remove_internal_membrane(&m, Vector3::new(0.0, 0.0, 0.0));
assert_eq!(out.indices.len() % 3, 0);
}
#[test]
fn remove_internal_membrane_deterministic_on_all_nan_extents() {
let build = || {
let mut m = Mesh::new();
for t in 0..4u32 {
let base = t * 3;
for _ in 0..3u32 {
m.positions.extend_from_slice(&[
f32::INFINITY,
f32::INFINITY,
f32::INFINITY,
]);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
m
};
let a = GeometryRouter::remove_internal_membrane(&build(), Vector3::new(0.0, 0.0, 0.0));
let b = GeometryRouter::remove_internal_membrane(&build(), Vector3::new(0.0, 0.0, 0.0));
assert_eq!(a.indices, b.indices, "all-NaN extents must pick a deterministic axis");
assert_eq!(a.positions.len(), b.positions.len());
assert_eq!(a.indices.len() % 3, 0);
}
#[test]
fn axis_pick_total_order_semantics() {
let pick = |ext: [f64; 3]| -> usize {
(0..3).max_by(|&i, &j| ext[i].total_cmp(&ext[j])).unwrap()
};
assert_eq!(pick([-0.0, 0.0, -1.0]), 1, "+0.0 outranks -0.0 in the total order");
assert_eq!(pick([0.0, 0.0, 0.0]), 2, "ties resolve to the last index");
assert_eq!(pick([f64::NAN, f64::NAN, f64::NAN]), 2, "all-NaN ties resolve to the last index");
assert_eq!(pick([f64::NAN, 1.0, 2.0]), 0, "positive NaN outranks finite values");
assert_eq!(pick([1.0, f64::INFINITY, f64::NAN]), 2, "positive NaN outranks +inf");
}
fn flip_winding(m: &Mesh) -> Mesh {
let mut o = m.clone();
for t in o.indices.chunks_exact_mut(3) {
t.swap(1, 2);
}
o
}
#[test]
fn an_inward_wound_host_is_read_the_same_as_an_outward_one() {
let up = Vector3::new(0.0, 0.0, 1.0);
let slab = GeometryRouter::make_box_mesh(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 1.0, 0.4));
let pocket =
GeometryRouter::make_box_mesh(Point3::new(0.5, 0.25, 0.15), Point3::new(1.5, 0.75, 0.4));
let clearance = |host: &Mesh| {
GeometryRouter::extend_opening_mesh_through_host(&pocket, host, up)
.bounds()
.1
.z as f64
- 0.4
};
let (outward, inward) = (clearance(&slab), clearance(&flip_winding(&slab)));
assert!(
(outward - inward).abs() < 1.0e-9 && inward > 0.0,
"an inward-wound host must get the same exit clearance as an outward one; \
outward {outward:.6}, inward {inward:.6}"
);
let cutter =
GeometryRouter::make_box_mesh(Point3::new(2.0, -10.0, 0.5), Point3::new(4.0, 10.4, 2.5));
let ext = GeometryRouter::extend_opening_mesh_through_host(
&cutter,
&flip_winding(&pre_cut_wall()),
Vector3::new(1.0, 0.0, 0.0),
);
let (mn, mx) = ext.bounds();
let band = 2.0f64 * 1.0e-3; assert!(
(mn.x as f64) >= 2.0 - 1.0e-6 && (mx.x as f64) <= 4.0 + 1.0e-6,
"an inward-wound pre-cut host must not have its jambs pushed into the piers; \
cutter spans {:.3} .. {:.3}, authored 2.000 .. 4.000",
mn.x,
mx.x
);
assert!(
(mn.x as f64) <= 2.0 + band + 1.0e-6 && (mx.x as f64) >= 4.0 - band - 1.0e-6,
"and it must not under-reach by more than the one band it is pulled in by; \
cutter spans {:.3} .. {:.3}",
mn.x,
mx.x
);
}
mod issue_4119_triangle_count_gate {
use super::*;
use ifc_lite_core::{build_entity_index, EntityScanner};
use std::fmt::Write as _;
fn build_unshared_grid_faceted_brep(start_id: u32, g: usize) -> (String, u32, u32) {
let mut out = String::new();
let mut next_id = start_id;
let mut face_ids: Vec<u32> = Vec::with_capacity(g * g);
for i in 0..g {
for j in 0..g {
let (x, y) = (i as f64, j as f64);
let mut corner_ids = [0u32; 4];
for (k, (dx, dy)) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
.into_iter()
.enumerate()
{
let pid = next_id;
let _ = writeln!(
out,
"#{pid}=IFCCARTESIANPOINT(({:.4},{:.4},0.0));",
x + dx,
y + dy
);
next_id += 1;
corner_ids[k] = pid;
}
let loop_id = next_id;
let _ = writeln!(
out,
"#{loop_id}=IFCPOLYLOOP((#{},#{},#{},#{}));",
corner_ids[0], corner_ids[1], corner_ids[2], corner_ids[3]
);
next_id += 1;
let bound_id = next_id;
let _ = writeln!(out, "#{bound_id}=IFCFACEOUTERBOUND(#{loop_id},.T.);");
next_id += 1;
let face_id = next_id;
let _ = writeln!(out, "#{face_id}=IFCFACE((#{bound_id}));");
next_id += 1;
face_ids.push(face_id);
}
}
let shell_id = next_id;
next_id += 1;
let refs: Vec<String> = face_ids.iter().map(|id| format!("#{id}")).collect();
let _ = writeln!(out, "#{shell_id}=IFCCLOSEDSHELL(({}));", refs.join(","));
let brep_id = next_id;
next_id += 1;
let _ = writeln!(out, "#{brep_id}=IFCFACETEDBREP(#{shell_id});");
(out, next_id, brep_id)
}
const PLATE_HEADER: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
FILE_NAME('test.ifc','2024-01-01T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('1234567890123456789012',#2,'Test',$,$,$,$,(#10),#7);
#2=IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,$,$,0);
#3=IFCPERSONANDORGANIZATION(#5,#6,$);
#4=IFCAPPLICATION(#6,'1.0','Test','Test');
#5=IFCPERSON($,'Test',$,$,$,$,$,$);
#6=IFCORGANIZATION($,'Test',$,$,$);
#7=IFCUNITASSIGNMENT((#8,#9));
#8=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#9=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#10=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#11,$);
#11=IFCAXIS2PLACEMENT3D(#12,$,$);
#12=IFCCARTESIANPOINT((0.,0.,0.));
#13=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#10,$,.MODEL_VIEW.,$);
#20=IFCLOCALPLACEMENT($,#21);
#21=IFCAXIS2PLACEMENT3D(#22,#23,#24);
#22=IFCCARTESIANPOINT((0.,0.,0.));
#23=IFCDIRECTION((0.,0.,1.));
#24=IFCDIRECTION((1.,0.,0.));
#30=IFCRECTANGLEPROFILEDEF(.AREA.,'Plate',#31,40.0,40.0);
#31=IFCAXIS2PLACEMENT2D(#32,#33);
#32=IFCCARTESIANPOINT((0.,0.));
#33=IFCDIRECTION((1.,0.));
#40=IFCEXTRUDEDAREASOLID(#30,#41,#42,2.0);
#41=IFCAXIS2PLACEMENT3D(#43,$,$);
#42=IFCDIRECTION((0.,0.,1.));
#43=IFCCARTESIANPOINT((0.,0.,0.));
#50=IFCSHAPEREPRESENTATION(#13,'Body','SweptSolid',(#40));
#51=IFCPRODUCTDEFINITIONSHAPE($,$,(#50));
#100=IFCPLATE('0001234567890123456789',#2,'TestPlate',$,$,#20,#51,'Tag',$);
#60=IFCLOCALPLACEMENT(#20,#61);
#61=IFCAXIS2PLACEMENT3D(#62,#63,#64);
#62=IFCCARTESIANPOINT((0.,0.,-1.));
#63=IFCDIRECTION((0.,0.,1.));
#64=IFCDIRECTION((1.,0.,0.));
"#;
fn plate_with_faceted_opening_ifc() -> String {
let (brep_entities, next_id, brep_id) = build_unshared_grid_faceted_brep(1000, 8);
let mut out = String::new();
out.push_str(PLATE_HEADER);
out.push_str(&brep_entities);
let rep_id = next_id;
let pds_id = next_id + 1;
let opening_id = next_id + 2;
let rel_id = next_id + 3;
let _ = writeln!(
out,
"#{rep_id}=IFCSHAPEREPRESENTATION(#13,'Body','Brep',(#{brep_id}));"
);
let _ = writeln!(out, "#{pds_id}=IFCPRODUCTDEFINITIONSHAPE($,$,(#{rep_id}));");
let _ = writeln!(
out,
"#{opening_id}=IFCOPENINGELEMENT('0001234567890123456790',#2,'Hole',$,$,#60,#{pds_id},$,.OPENING.);"
);
let _ = writeln!(
out,
"#{rel_id}=IFCRELVOIDSELEMENT('0001234567890123456791',#2,$,$,#100,#{opening_id});"
);
out.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
out
}
fn build_zigzag_triangle_strip_brep(start_id: u32, n: usize) -> (String, u32, u32, usize, usize) {
let mut out = String::new();
let mut next_id = start_id;
let mut face_ids: Vec<u32> = Vec::with_capacity(n);
let strip_point = |j: usize| -> (f64, f64) { ((j / 2) as f64, (j % 2) as f64) };
for i in 0..n {
let order: [usize; 3] = if i % 2 == 0 {
[i, i + 1, i + 2]
} else {
[i + 1, i, i + 2]
};
let mut corner_ids = [0u32; 3];
for (k, j) in order.into_iter().enumerate() {
let (x, y) = strip_point(j);
let pid = next_id;
let _ = writeln!(out, "#{pid}=IFCCARTESIANPOINT(({:.4},{:.4},0.0));", x, y);
next_id += 1;
corner_ids[k] = pid;
}
let loop_id = next_id;
let _ = writeln!(
out,
"#{loop_id}=IFCPOLYLOOP((#{},#{},#{}));",
corner_ids[0], corner_ids[1], corner_ids[2]
);
next_id += 1;
let bound_id = next_id;
let _ = writeln!(out, "#{bound_id}=IFCFACEOUTERBOUND(#{loop_id},.T.);");
next_id += 1;
let face_id = next_id;
let _ = writeln!(out, "#{face_id}=IFCFACE((#{bound_id}));");
next_id += 1;
face_ids.push(face_id);
}
let shell_id = next_id;
next_id += 1;
let refs: Vec<String> = face_ids.iter().map(|id| format!("#{id}")).collect();
let _ = writeln!(out, "#{shell_id}=IFCCLOSEDSHELL(({}));", refs.join(","));
let brep_id = next_id;
next_id += 1;
let _ = writeln!(out, "#{brep_id}=IFCFACETEDBREP(#{shell_id});");
let welded_vertex_count = n + 2;
let raw_vertex_count = 3 * n;
(out, next_id, brep_id, welded_vertex_count, raw_vertex_count)
}
fn plate_with_n_triangle_opening_ifc(n: usize) -> (String, usize, usize) {
let (brep_entities, next_id, brep_id, welded_vertex_count, raw_vertex_count) =
build_zigzag_triangle_strip_brep(1000, n);
let mut out = String::new();
out.push_str(PLATE_HEADER);
out.push_str(&brep_entities);
let rep_id = next_id;
let pds_id = next_id + 1;
let opening_id = next_id + 2;
let rel_id = next_id + 3;
let _ = writeln!(
out,
"#{rep_id}=IFCSHAPEREPRESENTATION(#13,'Body','Brep',(#{brep_id}));"
);
let _ = writeln!(out, "#{pds_id}=IFCPRODUCTDEFINITIONSHAPE($,$,(#{rep_id}));");
let _ = writeln!(
out,
"#{opening_id}=IFCOPENINGELEMENT('0001234567890123456790',#2,'Hole',$,$,#60,#{pds_id},$,.OPENING.);"
);
let _ = writeln!(
out,
"#{rel_id}=IFCRELVOIDSELEMENT('0001234567890123456791',#2,$,$,#100,#{opening_id});"
);
out.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
(out, welded_vertex_count, raw_vertex_count)
}
#[test]
fn triangle_count_gate_boundary() {
for &(n, takes_high_branch) in &[(99usize, false), (100usize, false), (101usize, true)] {
let (content, welded_vertex_count, raw_vertex_count) =
plate_with_n_triangle_opening_ifc(n);
let entity_index = build_entity_index(&content);
let mut decoder = EntityDecoder::with_index(&content, entity_index);
let router = GeometryRouter::with_units(&content, &mut decoder);
let opening_id = find_opening_id(&content, 100);
let plate = decoder.decode_by_id(100).expect("decode plate #100");
let openings = router.classify_openings(&plate, &[opening_id], &mut decoder);
assert_eq!(openings.len(), 1, "n={n}: exactly one opening classified");
let mesh = match &openings[0] {
OpeningType::NonRectangular(mesh, ..) => mesh,
other => panic!(
"n={n}: expected NonRectangular (a zigzag strip is never a \
clean box), got a different OpeningType variant: {}",
match other {
OpeningType::Rectangular(..) => "Rectangular",
OpeningType::DiagonalRectangular(..) => "DiagonalRectangular",
OpeningType::NonRectangular(..) => unreachable!(),
}
),
};
let triangle_count = mesh.indices.len() / 3;
assert_eq!(
triangle_count, n,
"n={n}: the embedded mesh's own triangle count should be unaffected \
by which branch was taken"
);
let vertex_count = mesh.positions.len() / 3;
if takes_high_branch {
assert_eq!(
vertex_count, welded_vertex_count,
"n={n}: triangle_count > 100 must take the `if` branch, whose \
mesh is the WELDED opening_mesh ({welded_vertex_count} \
vertices) — reading {vertex_count} instead means the gate \
let this n through to the per-item branch"
);
} else {
assert_eq!(
vertex_count, raw_vertex_count,
"n={n}: triangle_count <= 100 must take the `else` per-item \
branch, whose mesh is UNWELDED ({raw_vertex_count} vertices) \
— reading {vertex_count} instead means the gate incorrectly \
routed this n through the high-triangle-count branch"
);
}
}
}
fn find_opening_id(content: &str, host_id: u32) -> u32 {
let mut scanner = EntityScanner::new(content);
let mut decoder = EntityDecoder::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name != "IFCRELVOIDSELEMENT" {
continue;
}
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
if entity.get_ref(4) == Some(host_id) {
if let Some(opening_id) = entity.get_ref(5) {
return opening_id;
}
}
}
}
panic!("no IFCRELVOIDSELEMENT found for host #{host_id}");
}
#[test]
fn nonrectangular_opening_keeps_the_welded_mesh_not_the_raw_one() {
let content = plate_with_faceted_opening_ifc();
let entity_index = build_entity_index(&content);
let mut decoder = EntityDecoder::with_index(&content, entity_index);
let router = GeometryRouter::with_units(&content, &mut decoder);
let opening_id = find_opening_id(&content, 100);
let plate = decoder.decode_by_id(100).expect("decode plate #100");
let openings = router.classify_openings(&plate, &[opening_id], &mut decoder);
assert_eq!(openings.len(), 1, "exactly one opening classified");
let mesh = match &openings[0] {
OpeningType::NonRectangular(mesh, ..) => mesh,
other => panic!(
"expected NonRectangular (128 triangles, no inferable box/frame), \
got a different OpeningType variant: {}",
match other {
OpeningType::Rectangular(..) => "Rectangular",
OpeningType::DiagonalRectangular(..) => "DiagonalRectangular",
OpeningType::NonRectangular(..) => unreachable!(),
}
),
};
let triangle_count = mesh.indices.len() / 3;
let vertex_count = mesh.positions.len() / 3;
assert_eq!(triangle_count, 128, "8x8 grid = 64 cells x 2 triangles");
assert_eq!(
vertex_count, 81,
"expected the WELDED opening_mesh (9x9 = 81 grid points) — a \
vertex_count of {vertex_count} means the classifier took the \
unwelded per-item branch instead, i.e. the >100 gate is not \
reading triangle count"
);
}
}