1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! Watertightness tests for [`super::extrude_profile_watertight`] (the 2D
//! opening-subtraction re-extrude path, #1806). Split from `extrusion.rs`
//! inline tests to keep that module inside its size budget; `*_tests.rs`
//! files are ratchet-exempt.
use super::*;
use crate::profile::Profile2D;
/// Count UNDIRECTED edges NOT shared by exactly two triangles — 0 on a closed
/// 2-manifold (the production watertightness contract, `param_cut_watertight`).
/// Vertices welded by exact f32 bits (coincident verts share bit patterns).
fn open_edges(m: &Mesh) -> usize {
use std::collections::HashMap;
let key = |i: u32| {
let b = i as usize * 3;
(
m.positions[b].to_bits(),
m.positions[b + 1].to_bits(),
m.positions[b + 2].to_bits(),
)
};
let mut edges: HashMap<_, u32> = HashMap::new();
for t in m.indices.chunks_exact(3) {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
let (ka, kb) = (key(a), key(b));
let e = if ka < kb { (ka, kb) } else { (kb, ka) };
*edges.entry(e).or_insert(0) += 1;
}
}
edges.values().filter(|&&c| c != 2).count()
}
/// Total area of the triangles lying on the plane `z ≈ z0` (a cap), summed
/// with |signed area| so it is winding-independent.
fn cap_area(m: &Mesh, z0: f32) -> f64 {
let v = |i: u32| {
let b = i as usize * 3;
[m.positions[b], m.positions[b + 1], m.positions[b + 2]]
};
let mut a = 0.0;
for t in m.indices.chunks_exact(3) {
let (p, q, r) = (v(t[0]), v(t[1]), v(t[2]));
if (p[2] - z0).abs() < 1e-3 && (q[2] - z0).abs() < 1e-3 && (r[2] - z0).abs() < 1e-3 {
a += (((q[0] - p[0]) * (r[1] - p[1]) - (r[0] - p[0]) * (q[1] - p[1])) as f64).abs()
* 0.5;
}
}
a
}
/// The 2D opening-subtraction re-extrude must produce a WATERTIGHT solid even
/// for a many-hole profile — the case earcut hole-bridge slivers break (they
/// leave the cap non-manifold, then `clean_degenerate` cracks it). A 10×10
/// plate with a 4×4 grid of 0.4×0.4 through-holes, extruded 2 m: the CDT caps
/// close as a 2-manifold (every edge shared by two triangles) and each cap's
/// area equals outer − holes (the holes are genuinely cut, not filled).
#[test]
fn watertight_extrude_many_holes() {
use nalgebra::Point2;
let outer = vec![
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 10.0),
Point2::new(0.0, 10.0),
];
let mut profile = Profile2D::new(outer);
let mut hole_area = 0.0;
for i in 0..4 {
for j in 0..4 {
let (x, y) = (1.0 + i as f64 * 2.2, 1.0 + j as f64 * 2.2);
// Clockwise hole (opposite the CCW outer).
profile.add_hole(vec![
Point2::new(x, y),
Point2::new(x, y + 0.4),
Point2::new(x + 0.4, y + 0.4),
Point2::new(x + 0.4, y),
]);
hole_area += 0.4 * 0.4;
}
}
let depth = 2.0;
let mesh = extrude_profile_watertight(&profile, depth, None).unwrap();
assert_eq!(open_edges(&mesh), 0, "many-hole re-extrude must be a closed 2-manifold");
let expect = 100.0 - hole_area;
for (label, z) in [("bottom", 0.0f32), ("top", depth as f32)] {
assert!(
(cap_area(&mesh, z) - expect).abs() < 1e-3,
"{label} cap area {} != expected {expect} (holes not cut?)",
cap_area(&mesh, z)
);
}
}