Skip to content

Commit 833cce7

Browse files
BIMvoicelouistrue
andauthored
test(geometry): pin gap_boundary's per-edge half_thickness read (#3013) (#3050)
The surviving mutation was a question, not a verdict, and the answer is "untested", not "dead": `gap_boundary` is reached on every run -- `build_from_wall_rects` calls it at `factor = 1.0` for its axis lift -- and the read genuinely moves the output. Zeroing it drops the axis outline back onto the net gap, which the existing suite does catch. What nothing caught is WHICH edge the read points at. Every fixture in this file boxes its room with four walls of the SAME thickness, so permuting `half_thickness` around the cycle moves nothing at all. Verified on current main: rewriting the read as `cycle[(i + 1) % n]` leaves the crate at 708 passed, 0 failed. Area could not have closed this, which is the lesson #2913 recorded. Offsetting a rectangle by a permutation of the same four numbers only permutes which side each lands on, and the opposite-edge swap (`cycle[(i + 2) % n]`) is exactly area-preserving: width stays 4.8 and height stays 3.5 either way. So assert POSITION -- four distinct half-thicknesses and one assertion per side of the offset rectangle, each side displaced by exactly one edge's value, so any misrouting moves at least one of them. Bounds are taken as min/max rather than by vertex index, so the test pins the geometry rather than which corner the face cycle starts at, and the fixture asserts its own four values stay distinct -- a later edit making them uniform would silently restore the blind spot. Mutants killed (each verified individually, all six): - `cycle[(i + 1) % n]` -> left side at -0.10, want -0.55 - `cycle[(i + n - 1) % n]` -> left side at -0.40, want -0.55 - `cycle[(i + 2) % n]` -> left side at -0.25, want -0.55 (area-neutral) - `.half_thickness * 0.0` -> left side at 0, want -0.55 - `let off = half` -> factor=2 left side at -0.55, want -1.10 - `[-uy * off, ux * off]` -> left side at +0.55, want -0.55 (inset, not outset) Test-only: the read is correct as written, so there is no production change to make. `ifc-lite-geometry` lib 708 -> 709 passed, 0 failed; `cargo clippy -p ifc-lite-geometry --all-targets -- -D warnings` clean; module-size ratchet 5/5 (test files are exempt, ALLOWLIST_DIGEST unmoved). Co-authored-by: Louis Trümpler <78563314+louistrue@users.noreply.github.com>
1 parent 6095fe0 commit 833cce7

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

rust/geometry/src/space_dcel/tests.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,100 @@
5151
assert!((gross - 13.44).abs() < 1e-3, "gross (outer faces) = 13.44, got {gross}");
5252
}
5353

54+
/// `gap_boundary` must offset each edge by ITS OWN wall's half-thickness.
55+
///
56+
/// Every other fixture in this file gives all four walls the same thickness,
57+
/// which makes the per-edge read at the top of `gap_boundary` unobservable:
58+
/// permuting `half_thickness` around the cycle moves nothing, so the entire
59+
/// crate suite stayed green with that read pointing at the neighbouring edge
60+
/// (#3013). The read is reached -- `build_from_wall_rects` calls
61+
/// `gap_boundary` for its axis lift -- it just was not pinned.
62+
///
63+
/// Area does not separate the cases either: offsetting a rectangle by a
64+
/// permutation of the same four numbers only permutes which side each one
65+
/// lands on, and an opposite-side swap leaves the area identical. That is
66+
/// the lesson #2913 recorded -- assert a POSITION the mutation actually
67+
/// moves.
68+
///
69+
/// So: four DISTINCT half-thicknesses and an assertion per side. Each side
70+
/// of the offset rectangle is displaced by exactly one edge's
71+
/// half-thickness, so any misrouting of the read moves at least one side.
72+
/// Bounds are taken as min/max rather than by vertex index, so this pins the
73+
/// geometry and not which corner the face cycle happens to start at.
74+
#[test]
75+
fn gap_boundary_offsets_each_edge_by_its_own_half_thickness() {
76+
// CCW 4x3 centreline room; `rect` yields bottom, right, top, left.
77+
let corners = rect(0.0, 0.0, 4.0, 3.0);
78+
let halves: [f64; 4] = [0.10, 0.25, 0.40, 0.55]; // bottom, right, top, left
79+
// The fixture can only see the defect while these differ; a future edit
80+
// making them uniform would silently restore the blind spot this test
81+
// exists to close.
82+
for i in 0..halves.len() {
83+
for j in (i + 1)..halves.len() {
84+
assert!(
85+
(halves[i] - halves[j]).abs() > 1e-9,
86+
"the four half-thicknesses must stay distinct, or a permuted \
87+
read becomes unobservable again"
88+
);
89+
}
90+
}
91+
let segs: Vec<InputSegment> = (0..4)
92+
.map(|i| {
93+
InputSegment::new(corners[i], corners[(i + 1) % 4], Some(200 + i as u32))
94+
.with_half_thickness(halves[i])
95+
})
96+
.collect();
97+
let plate = SpacePlate::build(&segs, BuildOptions::default());
98+
assert_eq!(plate.room_count(), 1);
99+
let room = plate.rooms().next().unwrap();
100+
101+
let bounds = |ring: &[[f64; 2]]| {
102+
ring.iter().fold(
103+
[f64::MAX, f64::MIN, f64::MAX, f64::MIN],
104+
|[lo_x, hi_x, lo_y, hi_y], p| {
105+
[lo_x.min(p[0]), hi_x.max(p[0]), lo_y.min(p[1]), hi_y.max(p[1])]
106+
},
107+
)
108+
};
109+
// Sanity: the un-offset outline is the centreline rectangle itself.
110+
let [cx0, cx1, cy0, cy1] = bounds(&plate.face_outline(room));
111+
for (got, want, what) in
112+
[(cx0, 0.0, "left"), (cx1, 4.0, "right"), (cy0, 0.0, "bottom"), (cy1, 3.0, "top")]
113+
{
114+
assert!((got - want).abs() < 1e-9, "centreline {what} = {want}, got {got}");
115+
}
116+
117+
// factor = 1 pushes each side out by that side's own half-thickness.
118+
let [x0, x1, y0, y1] = bounds(&plate.gap_boundary(room, 1.0));
119+
for (got, want, what) in [
120+
(x0, -0.55, "left (half 0.55)"),
121+
(x1, 4.25, "right (half 0.25)"),
122+
(y0, -0.10, "bottom (half 0.10)"),
123+
(y1, 3.40, "top (half 0.40)"),
124+
] {
125+
assert!(
126+
(got - want).abs() < 1e-6,
127+
"factor=1: {what} side must sit at {want}, got {got} — a side at \
128+
another edge's offset means the per-edge half_thickness read is \
129+
misrouted"
130+
);
131+
}
132+
133+
// factor = 2 must scale each side's OWN offset, not a shared one.
134+
let [dx0, dx1, dy0, dy1] = bounds(&plate.gap_boundary(room, 2.0));
135+
for (got, want, what) in [
136+
(dx0, -1.10, "left (2 x 0.55)"),
137+
(dx1, 4.50, "right (2 x 0.25)"),
138+
(dy0, -0.20, "bottom (2 x 0.10)"),
139+
(dy1, 3.80, "top (2 x 0.40)"),
140+
] {
141+
assert!(
142+
(got - want).abs() < 1e-6,
143+
"factor=2: {what} side must sit at {want}, got {got}"
144+
);
145+
}
146+
}
147+
54148
#[test]
55149
fn face_based_edits_preserve_room_classification() {
56150
// The 4-wall box → one gap room. `is_room` is set once at build and

0 commit comments

Comments
 (0)