ifc-lite-geometry 14.0.0

Geometry processing and mesh generation for IFC models
Documentation
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
// 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/.

//! Tests for `mesh_bridge` — split out of the module to keep it under its
//! size ratchet budget (`_tests.rs` siblings are ratchet-exempt).

use super::super::arrangement::cube_mesh;
use super::*;

fn mesh_volume(m: &Mesh) -> f64 {
    let vertex = |i: u32| {
        let b = (i as usize) * 3;
        [
            m.positions[b] as f64,
            m.positions[b + 1] as f64,
            m.positions[b + 2] as f64,
        ]
    };
    m.indices
        .chunks_exact(3)
        .map(|c| {
            let (a, bb, cc) = (vertex(c[0]), vertex(c[1]), vertex(c[2]));
            let cr = [
                bb[1] * cc[2] - bb[2] * cc[1],
                bb[2] * cc[0] - bb[0] * cc[2],
                bb[0] * cc[1] - bb[1] * cc[0],
            ];
            a[0] * cr[0] + a[1] * cr[1] + a[2] * cr[2]
        })
        .sum::<f64>()
        / 6.0
}

fn expect_cut(outcome: BatchSubtract, what: &str) -> Mesh {
    match outcome {
        BatchSubtract::Cut(m) => m,
        other => panic!("{what}: expected BatchSubtract::Cut, got {other:?}"),
    }
}

#[test]
fn snap_reconciles_near_coplanar_and_is_deterministic() {
    // coords closer than the grid snap to the SAME value (f32-flush → exact)
    assert_eq!(super::snap(1.0), super::snap(1.0 + 1e-6));
    assert_eq!(super::snap(2.5), super::snap(2.5 - 5e-6));
    // grid multiples (incl. integers) are exact fixed points
    assert_eq!(super::snap(3.0), 3.0);
    assert_eq!(super::snap(0.0), 0.0);
    assert_eq!(super::snap(7.0 / 65536.0), 7.0 / 65536.0);
    // distinct grid cells stay distinct
    assert_ne!(super::snap(1.0), super::snap(1.0 + 1e-3));
}

/// `mesh_to_tris` is documented panic-free against a triangle whose vertex
/// index runs past the end of `positions` (a truncated/corrupt buffer):
/// the offending triangle is silently dropped rather than indexing OOB.
#[test]
fn mesh_to_tris_drops_out_of_range_index_without_panicking() {
    let mut m = Mesh::new();
    // one real triangle (verts 0,1,2)...
    m.positions.extend_from_slice(&[0.0, 0.0, 0.0]);
    m.positions.extend_from_slice(&[1.0, 0.0, 0.0]);
    m.positions.extend_from_slice(&[0.0, 1.0, 0.0]);
    // ...then a second face referencing vertex index 5, which is past the
    // end of a 3-vertex positions buffer (truncated/corrupt data).
    m.indices.extend_from_slice(&[0, 1, 2, 0, 1, 5]);

    let tris = mesh_to_tris(&m);

    assert_eq!(tris.len(), 1, "malformed triangle (OOB index) must be dropped, not panic");
    assert_eq!(tris[0], [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
}

/// `mesh_to_tris` is documented panic-free against a non-finite (NaN/Inf)
/// position coordinate: the offending triangle is silently dropped rather
/// than propagating NaN into the exact-predicate kernel.
#[test]
fn mesh_to_tris_drops_non_finite_coordinate_without_panicking() {
    let mut m = Mesh::new();
    // valid triangle (verts 0,1,2)
    m.positions.extend_from_slice(&[0.0, 0.0, 0.0]);
    m.positions.extend_from_slice(&[1.0, 0.0, 0.0]);
    m.positions.extend_from_slice(&[0.0, 1.0, 0.0]);
    // NaN-poisoned vertex 3, referenced by a second face
    m.positions.extend_from_slice(&[f32::NAN, 0.0, 0.0]);
    // Inf-poisoned vertex 4, referenced by a third face
    m.positions.extend_from_slice(&[f32::INFINITY, 0.0, 0.0]);
    m.indices
        .extend_from_slice(&[0, 1, 2, 0, 1, 3, 0, 1, 4]);

    let tris = mesh_to_tris(&m);

    assert_eq!(
        tris.len(),
        1,
        "triangles touching a NaN or Inf coordinate must be dropped, not panic"
    );
    assert_eq!(tris[0], [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
}

#[test]
fn kernel_cuts_a_real_mesh() {
    // Round-trip through ifc-lite's Mesh: two cube meshes, subtract via the
    // kernel, and the result Mesh has the exact box−box volume.
    let host = tris_to_mesh(&cube_mesh(0.0, 2.0)); // vol 8
    let cutter = tris_to_mesh(&cube_mesh(1.0, 3.0)); // overlap [1,2]³ = 1
    let result = subtract(&host, &cutter);
    assert!(!result.indices.is_empty(), "subtract produced an empty mesh");
    let v = mesh_volume(&result);
    assert!((v - 7.0).abs() < 1e-3, "Mesh host−cutter volume = {v}, expected 7");
    // sanity: the round-tripped host mesh has volume 8
    assert!((mesh_volume(&host) - 8.0).abs() < 1e-4, "host round-trip volume wrong");
}

#[test]
fn kernel_cuts_a_through_wall_opening() {
    use super::super::arrangement::box_mesh;
    // a thin wall slab with a box opening poking all the way through (z)
    let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [4., 3., 0.2])); // vol 2.4
    let opening = tris_to_mesh(&box_mesh([1., 1., -0.5], [2., 2., 0.7])); // hole vol 0.2
    let result = subtract(&wall, &opening);
    let v = mesh_volume(&result);
    assert!((v - 2.2).abs() < 1e-3, "through-opening wall volume = {v}, expected 2.2");
}

/// Extended-cutter-graze regression (a rotated tunnel-wall fixture): a
/// rotated 12-tri host box minus the cutter box that
/// `extend_opening_mesh_through_host` pushed through it. The push slid a
/// bit-exactly-shared corner ALONG the host end-face plane; the f32 round
/// left it ~8 µm off (a tilt the per-axis snap can't flatten), so a host
/// edge GRAZED the cutter jamb face and the subtract emitted 27 tris /
/// 13 open edges / signed volume −4.268 (vs Manifold's +3.182871 on the
/// SAME operands). The cross-operand promotion welds the slid corner back
/// onto the host plane; the cut must be watertight with the oracle volume.
#[test]
fn extended_cutter_graze_subtracts_exactly() {
    fn mesh_of(vs: &[[f32; 3]], fs: &[[u32; 3]]) -> Mesh {
        let mut m = Mesh::new();
        for v in vs {
            m.positions.extend_from_slice(v);
            m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
        }
        for f in fs {
            m.indices.extend_from_slice(f);
        }
        m
    }
    // exact f32 coords as dumped from the failing host/cutter pair;
    // 8 unique verts each, both watertight.
    let host = mesh_of(
        &[
            [274.05923, 400.96225, 34.600006],
            [276.68744, 404.85873, 34.600006],
            [276.52164, 404.97058, 34.600006],
            [274.00525, 401.2399, 34.600006],
            [274.05923, 400.96225, 38.600006],
            [276.68744, 404.85873, 38.600006],
            [276.52164, 404.97058, 38.600006],
            [274.00525, 401.2399, 38.600006],
        ],
        &[
            [3, 1, 0], [1, 3, 2], [7, 4, 5], [5, 6, 7], [0, 1, 5], [0, 5, 4],
            [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
        ],
    );
    let cutter = mesh_of(
        &[
            [277.01904, 404.63507, 34.6],
            [276.39276, 403.70654, 34.6],
            [276.39276, 403.70654, 36.82],
            [277.01904, 404.63507, 36.82],
            [276.3724, 405.07123, 34.6],
            [275.7461, 404.1427, 34.6],
            [275.7461, 404.1427, 36.82],
            [276.3724, 405.07123, 36.82],
        ],
        &[
            [2, 0, 3], [0, 2, 1], [6, 7, 4], [4, 5, 6], [0, 1, 5], [0, 5, 4],
            [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
        ],
    );
    assert!((mesh_volume(&host) - 3.680154).abs() < 1e-4, "host operand changed");
    assert!((mesh_volume(&cutter) - 1.939390).abs() < 1e-4, "cutter operand changed");
    let result = subtract(&host, &cutter);
    let v = mesh_volume(&result);
    // Manifold oracle on the same operands: +3.182871 (pure on the
    // UNextended cutter: +3.18291). f32 round-trip noise stays ≪ 1e-3.
    assert!((v - 3.182871).abs() < 1e-3, "subtract volume = {v}, expected ≈3.182871");
    // watertight: every directed edge must be paired (the broken cut had 13 bad)
    let s = 1e5_f32;
    let key = |i: u32| {
        let b = i as usize * 3;
        (
            (result.positions[b] * s).round() as i64,
            (result.positions[b + 1] * s).round() as i64,
            (result.positions[b + 2] * s).round() as i64,
        )
    };
    let mut edges = std::collections::HashMap::new();
    for t in result.indices.chunks_exact(3) {
        for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
            *edges.entry((key(a), key(b))).or_insert(0i32) += 1;
            *edges.entry((key(b), key(a))).or_insert(0i32) -= 1;
        }
    }
    let bad = edges.values().filter(|&&c| c != 0).count();
    assert_eq!(bad, 0, "result has {bad} unpaired directed edges");
}

/// Directed-edge pairing audit at EXACT f32-bit coordinates — the crack
/// detector. A watertight oriented surface has every directed edge matched
/// by its reverse; any imbalance is an exact-coordinate boundary crack
/// (the crack family). No rounding: two seam vertices that differ by
/// even one ULP count as a crack, which is precisely the defect.
fn exact_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<_, i64> = 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])] {
            *edges.entry((key(a), key(b))).or_insert(0) += 1;
            *edges.entry((key(b), key(a))).or_insert(0) -= 1;
        }
    }
    edges.values().filter(|&&c| c != 0).count()
}

fn mesh_of(vs: &[[f32; 3]], fs: &[[u32; 3]]) -> Mesh {
    let mut m = Mesh::new();
    for v in vs {
        m.positions.extend_from_slice(v);
        m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
    }
    for f in fs {
        m.indices.extend_from_slice(f);
    }
    m
}

/// Standard 8-vert box/prism face table (bottom quad 0-3, top quad 4-7).
const PRISM_FACES: [[u32; 3]; 12] = [
    [3, 1, 0], [1, 3, 2], [7, 4, 5], [5, 6, 7], [0, 1, 5], [0, 5, 4],
    [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
];
const BOX_FACES: [[u32; 3]; 12] = [
    [2, 0, 3], [0, 2, 1], [6, 7, 4], [4, 5, 6], [0, 1, 5], [0, 5, 4],
    [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
];

/// Crack-family regression (a far-from-origin tunnel wall, step 0 of the
/// minimal repro): a clean 12-tri plan-rotated wall prism minus ONE small
/// recess box whose jamb face is intended-flush with the TILTED host end
/// plane (they share the host corner (300.84857, 362.50748) bit-exactly;
/// the other jamb verts sit ~24 µm off-plane after the per-axis 2^-16
/// snap). Pre-fix the near-coplanar carve ran on the still-off-plane
/// coordinates, the A/B seam vertices never interned identically, and the
/// cut emitted 16 exact-coordinate open edges with volume 0.107 instead of
/// the analytic 0.306705. The exact-plane lift welds the jamb verts EXACTLY
/// onto the host plane so the coplanar carve conforms: watertight + exact.
#[test]
fn tilted_flush_recess_cut_is_watertight_198779() {
    let host = mesh_of(
        &[
            [301.04767, 363.11743, 47.6],
            [300.70264, 362.6059, 47.6],
            [300.84857, 362.50748, 47.6],
            [301.24, 363.08783, 47.6],
            [301.04767, 363.11743, 50.25],
            [300.70264, 362.6059, 50.25],
            [300.84857, 362.50748, 50.25],
            [301.24, 363.08783, 50.25],
        ],
        &PRISM_FACES,
    );
    let cutter = mesh_of(
        &[
            [300.85583, 362.51828, 47.6],
            [300.84506, 362.52554, 47.6],
            [300.8378, 362.51477, 47.6],
            [300.84857, 362.50748, 47.6],
            [300.85583, 362.51828, 50.25],
            [300.84506, 362.52554, 50.25],
            [300.8378, 362.51477, 50.25],
            [300.84857, 362.50748, 50.25],
        ],
        &BOX_FACES,
    );
    let result = subtract(&host, &cutter);
    let open = exact_open_edges(&result);
    assert_eq!(open, 0, "tilted-flush recess cut left {open} exact-coordinate open edges");
    let v = mesh_volume(&result);
    assert!(
        (v - 0.306705).abs() < 1e-3,
        "recess cut volume = {v}, expected ≈0.306705 (analytic; pre-fix 0.107)"
    );
}

/// Crack-family regression (the +10.3% max-error row of the far-from-origin
/// sweep): an 8×8-tri body `IfcBooleanClippingResult` DIFF in
/// native units (|y|≈6699 ⇒ one f32 ULP = 4.88e-4 ≈ 32 snap cells, so the
/// per-axis 2^-16 snap is structurally unable to reconcile the flush slant
/// plane). The cutter's bottom face is intended-flush with the host's slant
/// plane but 1 ULP off; pre-fix the cut emitted a 13-tri open result (16
/// exact open edges) at +10.3% vs IfcOpenShell. Post-fix the kernel output
/// is exactly closed at f32 with volume at IOS parity: 5868311718 mm³ =
/// 5.8683117 m³ vs IfcOpenShell 0.8.2's 5.868313 m³ (2.6e-7 relative).
#[test]
fn native_unit_flush_slant_diff_is_watertight_387738() {
    let host = mesh_of(
        &[
            [478.50012207031250, -0.0000457763671875, 0.0],
            [3580.001708984375, -6699.17578125, 167.4681396484375],
            [-1764.322265625, -6699.17578125, 167.4681396484375],
            [478.50012207031250, -0.0000457763671875, 499.907318115234375],
            [-1764.322265625, -6699.17578125, 667.37548828125],
            [3580.001708984375, -6699.17578125, 667.37548828125],
        ],
        &[
            [0, 1, 2], [3, 4, 5], [2, 1, 5], [2, 5, 4],
            [1, 0, 3], [1, 3, 5], [0, 2, 4], [0, 4, 3],
        ],
    );
    let cutter = mesh_of(
        &[
            [-1764.322113037109375, -6699.175338745117188, 283.737548828125],
            [478.50012207031250, -0.0000457763671875, 283.737548828125],
            [478.50012207031250, -0.0000457763671875, 0.0],
            [-1764.322265625, -6699.17529296875, 167.468124389648438],
            [3580.0015258789062, -6699.175384521484375, 283.737548828125],
            [3580.001708984375, -6699.17529296875, 167.468124389648438],
        ],
        &[
            [0, 1, 2], [0, 2, 3], [4, 1, 0], [1, 4, 5],
            [1, 5, 2], [5, 3, 2], [4, 0, 3], [4, 3, 5],
        ],
    );
    let host_vol = mesh_volume(&host);
    let result = subtract(&host, &cutter);
    let open = exact_open_edges(&result);
    assert_eq!(open, 0, "flush-slant DIFF left {open} exact-coordinate open edges");
    let v = mesh_volume(&result);
    // The kept part is the host above the flush z≈283.74 cut plane
    // (pre-fix: open 13-tri garbage at +10.3% vs the oracle).
    assert!(
        v > 0.0 && v < host_vol,
        "DIFF volume {v} not inside (0, host {host_vol})"
    );
    let expected = 5.868313e9_f64; // IfcOpenShell 0.8.2, mm³
    assert!(
        (v - expected).abs() / expected < 1e-5,
        "DIFF volume = {v}, expected ≈{expected} (IfcOpenShell oracle)"
    );
}

#[test]
fn kernel_cuts_two_sequential_openings() {
    use super::super::arrangement::box_mesh;
    // The void-router pattern: a host cut by several openings in sequence,
    // each subtract's OUTPUT fed back in as the next host.
    let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [6., 3., 0.2])); // vol 3.6
    let op1 = tris_to_mesh(&box_mesh([1., 1., -0.5], [2., 2., 0.7])); // hole 0.2
    let op2 = tris_to_mesh(&box_mesh([4., 1., -0.5], [5., 2., 0.7])); // hole 0.2
    let after2 = subtract(&subtract(&wall, &op1), &op2);
    let v = mesh_volume(&after2);
    assert!((v - 3.2).abs() < 1e-3, "two-opening wall volume = {v}, expected 3.2");
}

/// Tangential-touch conformity regression (the `TriTri::Point` fix): a
/// window box whose top-left corner lands EXACTLY on the host face
/// triangle's diagonal (z = x/2 at x=4). The lower face triangle sees the
/// window-top intersection as a SEGMENT ending on the diagonal and splits
/// it there; the upper triangle's intersection with the window top is just
/// that single POINT — pre-fix it was discarded, the upper triangle never
/// split its edge, and the resulting T-junction opened 12 exact-coordinate
/// edges on a plain binary subtract. The touch point is now interned as a
/// conformity vertex in BOTH triangles (`RetriInput::points`).
#[test]
fn tangential_touch_on_host_diagonal_is_watertight() {
    use super::super::arrangement::box_mesh;
    let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [6., 0.2, 3.])); // vol 3.6
    let window = tris_to_mesh(&box_mesh([4., -0.3, 0.5], [5., 0.5, 2.0])); // corner on diag
    let result = subtract(&wall, &window);
    let open = exact_open_edges(&result);
    assert_eq!(open, 0, "tangential-touch cut left {open} exact open edges");
    let v = mesh_volume(&result);
    assert!((v - 3.3).abs() < 1e-3, "window cut volume = {v}, expected 3.3");
}

/// Batching: a two-pocket batched group (flush-bottom door + a window whose
/// corner touches the face diagonal) must equal the sequential chain and
/// stay watertight — the configuration that exposed both the tangential-
/// touch defect and the swallowed-endpoint constraint-recovery bail.
#[test]
fn subtract_many_two_pocket_group_matches_sequential() {
    use super::super::arrangement::box_mesh;
    let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [6., 0.2, 3.])); // vol 3.6
    let door = tris_to_mesh(&box_mesh([1., -1.0, 0.0], [2., 1.2, 2.5])); // flush bottom
    let window = tris_to_mesh(&box_mesh([4., -0.3, 0.5], [5., 0.5, 2.0]));
    let seq = subtract(&subtract(&wall, &door), &window);
    let many = expect_cut(subtract_many(&wall, &[&door, &window]), "group must conform");
    let (vs, vm) = (mesh_volume(&seq), mesh_volume(&many));
    let om = exact_open_edges(&many);
    assert_eq!(om, 0, "batched two-pocket cut left {om} exact open edges");
    assert!(
        (vs - vm).abs() < 1e-6,
        "batched volume {vm} != sequential volume {vs} on disjoint cutters"
    );
}

/// Disjoint-cutter batching: `subtract_many` of three pairwise-
/// disjoint through-openings in ONE arrangement equals the sequential
/// per-cutter chain (analytic volume), is watertight, and is robust to a
/// component arriving INWARD-wound — the per-component orientation inside
/// `subtract_many` must fix it (a global signed-volume orientation of the
/// concatenated soup cannot; the #2176 lesson).
#[test]
fn subtract_many_disjoint_openings_matches_sequential() {
    use super::super::arrangement::box_mesh;
    let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [9., 3., 0.2])); // vol 5.4
    let op1 = tris_to_mesh(&box_mesh([1., 1., -0.5], [2., 2., 0.7])); // hole 0.2
    let mut op2 = tris_to_mesh(&box_mesh([4., 1., -0.5], [5., 2., 0.7])); // hole 0.2
    let op3 = tris_to_mesh(&box_mesh([7., 1., -0.5], [8., 2., 0.7])); // hole 0.2
    // flip op2's winding inward — per-component orientation must recover it
    for t in op2.indices.chunks_exact_mut(3) {
        t.swap(1, 2);
    }
    let batched = expect_cut(
        subtract_many(&wall, &[&op1, &op2, &op3]),
        "disjoint box group must conform",
    );
    let v = mesh_volume(&batched);
    assert!((v - 4.8).abs() < 1e-3, "batched 3-opening wall volume = {v}, expected 4.8");
    let open = exact_open_edges(&batched);
    assert_eq!(open, 0, "batched cut left {open} exact-coordinate open edges");
    // parity with the sequential chain
    let seq = subtract(&subtract(&subtract(&wall, &op1), &op2), &op3);
    let vs = mesh_volume(&seq);
    assert!(
        (v - vs).abs() < 1e-6,
        "batched volume {v} != sequential volume {vs} on disjoint cutters"
    );
}

/// A cutter whose AABB overlaps the host but whose solid never reaches it used
/// to come back as `Some(host re-tessellated)`, the same shape as a real cut,
/// and the router re-derived "did it cut" from a triangle count and a 0.1 %
/// volume gate (`voids/sweep.rs`, repaired twice under #1788). The classifier
/// now says it itself: every host sub-triangle kept and no cutter face kept is
/// [`BatchSubtract::Unchanged`]. Mutation: force `changed = true` in
/// `boolean_vids_components` and the first assertion reads `Cut`; force it
/// `false` and the second reads `Unchanged`.
#[test]
fn subtract_many_reports_unchanged_when_no_cutter_reaches_the_host() {
    let host = tris_to_mesh(&cube_mesh(0.0, 1.0));
    // Tetrahedron in the x + y > 2.2 corner of the host's AABB: its own AABB
    // [0.1, 2.1]^2 x [0.5, 1.5] overlaps the cube, its solid does not.
    let tetra = |dx: f32| {
        mesh_of(
            &[
                [2.1 + dx, 0.1 + dx, 0.5],
                [0.1 + dx, 2.1 + dx, 0.5],
                [2.1 + dx, 2.1 + dx, 0.5],
                [2.1 + dx, 2.1 + dx, 1.5],
            ],
            &[[0, 1, 2], [0, 3, 1], [1, 3, 2], [2, 3, 0]],
        )
    };
    let disjoint = tetra(0.0);
    assert!(
        matches!(subtract_many(&host, &[&disjoint]), BatchSubtract::Unchanged),
        "a cutter that misses the host solid must read Unchanged, not Cut"
    );
    // The same tetrahedron slid into the cube is a real cut: volume drops.
    let reaching = tetra(-0.6);
    let cut = expect_cut(subtract_many(&host, &[&reaching]), "reaching tetra");
    let v = mesh_volume(&cut).abs();
    assert!(v < 0.99 && v > 0.5, "reaching tetra must remove volume: {v}");
}

/// Issue #3353, the N-ary half.
///
/// `union_many` reaches the same broadphase, `near_coplanar` guard and
/// classifier as the binary union, so the per-axis snap that leaves two flush
/// faces a few µm apart tears it the same way — and `union_many` is the
/// PRIMARY union in the pipeline (`processors/boolean` builds the cutter union
/// with it, `coaxial_union` behind that), not a side path.
///
/// # Why the binary fix did not simply extend here, and what was actually wrong
///
/// Applying `promote_operands_mutually` in `union_many` DOES help this family:
/// a three-box near-coplanar sweep (49 corner placements x 3 snap offsets)
/// tears 105 of 147 without it and 36 of 147 with it, and this fixture goes
/// from 20 unmatched directed edges to 0.
///
/// Naively applying it also broke `tests/issue_960_segmented_roof_clip.rs`:
/// wall #4148 came back 9850 mm tall against an expected ~8984 mm — the
/// sequential fallback's full-height seam sliver, the exact defect #960
/// removed. The mechanism (confirmed by instrumenting a debug build against
/// the #960 fixture, not inferred): the roof-segment cutter prisms are
/// authored analytically and already share BIT-IDENTICAL vertices at their
/// true adjacency seams — no reconciliation needed there. But
/// `promote_cutter_verts_onto_host_faces`'s per-vertex plane search
/// deliberately EXCLUDES a vertex's own exact-match plane from candidacy
/// (`d == 0.0 { continue }`, load-bearing for the tunnel-wall jamb-corner
/// case elsewhere in this module) and then searches every OTHER host face
/// within band for a nearer one. With `host` pooling many roof segments that
/// share the same pitch (hence near-parallel planes), that search finds a
/// spurious near-match on an unrelated, non-adjacent operand's plane and
/// nudges the vertex a few µm off its true neighbour — measured on the #960
/// fixture's four `union_many` calls: the count of bit-identical vertex pairs
/// shared between operand pairs drops from (148, 4, 12, 248) pre-weld to
/// (88, 4, 5, 159) post-weld. The weld was actively DESTROYING pre-existing
/// exact seams, not merely reconciling noisy ones. The fix
/// (`promote_cutter_verts_onto_host_faces` in `plane_weld.rs`) skips any
/// cutter vertex that is already bit-identical to some host vertex — safe
/// everywhere, since a genuinely-imprecise cutter vertex (the tunnel-wall
/// case this guard was designed around) is by construction never in that
/// set.
///
/// These live in-crate rather than beside
/// `tests/issue_3353_near_coplanar_rotated_overlap.rs` because the production
/// combination is `consolidate_coplanar(union_many(..))` and
/// `consolidate_coplanar` is `pub(crate)`. Asserting the RAW `union_many`
/// output instead would assert the wrong thing: raw N-ary output carries
/// T-junctions that consolidation is expected to close (at `dz = 0` this same
/// three-box fixture is 26 open edges raw and 0 consolidated), so a raw
/// assertion would fail on geometry that is fine.
mod issue_3353_nary_near_coplanar {
    use super::*;
    use crate::csg::ClippingProcessor;
    use nalgebra::{Point3, Rotation3, Unit, Vector3};
    use std::collections::HashMap;

    /// `SNAP_GRID`, spelled out so the fixture is visibly scaled to the grid.
    const SG: f64 = 1.0 / 65536.0;

    fn boxed(min: [f64; 3], size: [f64; 3], rot: Option<(Vector3<f64>, f64, [f64; 3])>) -> Mesh {
        let mx = [min[0] + size[0], min[1] + size[1], min[2] + size[2]];
        let c = |i: usize| -> [f64; 2] { [min[i], mx[i]] };
        let mut corners: Vec<Point3<f64>> = [
            (0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0),
            (0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1),
        ]
        .iter()
        .map(|&(i, j, k)| Point3::new(c(0)[i], c(1)[j], c(2)[k]))
        .collect();
        if let Some((axis, angle, about)) = rot {
            let r = Rotation3::from_axis_angle(&Unit::new_normalize(axis), angle);
            let o = Point3::new(about[0], about[1], about[2]);
            for p in corners.iter_mut() {
                *p = o + r * (*p - o);
            }
        }
        let faces: [[usize; 4]; 6] = [
            [0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4],
            [2, 3, 7, 6], [0, 4, 7, 3], [1, 2, 6, 5],
        ];
        let mut m = Mesh::with_capacity(24, 36);
        for f in &faces {
            let e1 = corners[f[1]] - corners[f[0]];
            let e2 = corners[f[2]] - corners[f[0]];
            let n = e1.cross(&e2).try_normalize(1e-12).unwrap_or(Vector3::z());
            let b = m.vertex_count() as u32;
            for &i in f {
                m.add_vertex(corners[i], n);
            }
            m.add_triangle(b, b + 1, b + 2);
            m.add_triangle(b, b + 2, b + 3);
        }
        m
    }

    /// Unmatched directed edges after welding by position at 0.1 mm — the same
    /// census check the `issue_3353_*` integration tests use.
    fn open_edges(m: &Mesh) -> Result<usize, String> {
        if m.is_empty() {
            return Err("union produced nothing".to_string());
        }
        let w = m.welded_by_position(1e-4);
        let mut edges: HashMap<(u32, u32), (u32, u32)> = HashMap::new();
        for t in w.indices.chunks_exact(3) {
            for k in 0..3 {
                let (a, b) = (t[k], t[(k + 1) % 3]);
                if a == b {
                    return Err(format!("degenerate edge: triangle repeats welded vertex {a}"));
                }
                let e = edges.entry((a.min(b), a.max(b))).or_insert((0, 0));
                if a < b {
                    e.0 += 1;
                } else {
                    e.1 += 1;
                }
            }
        }
        Ok(edges.values().filter(|&&(f, r)| f != 1 || r != 1).count())
    }

    /// A axis-aligned at the origin; B rotated +30 degrees about Z overlapping
    /// its +X+Y corner; C rotated -20 degrees overlapping its -X+Y corner. Both
    /// rotated boxes sit `dz` above A, so TWO of the three horizontal face
    /// pairs are near-coplanar rather than flush.
    fn three_boxes(dz: f64) -> [Mesh; 3] {
        let a = boxed([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], None);
        let b = boxed(
            [0.4, 0.4, dz],
            [1.0, 1.0, 1.0],
            Some((Vector3::z(), 30.0f64.to_radians(), [0.9, 0.9, 0.5 + dz])),
        );
        let c = boxed(
            [-0.4, 0.4, dz],
            [1.0, 1.0, 1.0],
            Some((Vector3::z(), -20.0f64.to_radians(), [0.1, 0.9, 0.5 + dz])),
        );
        [a, b, c]
    }

    /// One snap step of offset: 20 unmatched directed edges before the weld
    /// reached `union_many`, closed after. `dz = 0` is the control — exactly
    /// flush was always clean, which is what names the near-coplanar regime.
    #[test]
    fn a_three_operand_near_coplanar_union_stays_closed() {
        for dz in [SG, 0.0] {
            let [a, b, c] = three_boxes(dz);
            for (name, m) in [("A", &a), ("B", &b), ("C", &c)] {
                assert_eq!(open_edges(m), Ok(0), "operand {name} must be closed going in");
            }
            // Every ordering: `union_many` has no privileged first operand, and
            // the mutual weld walks the operands in index order, so the result
            // must not depend on which one the caller lists first.
            for order in [
                [&a, &b, &c], [&a, &c, &b], [&b, &a, &c],
                [&b, &c, &a], [&c, &a, &b], [&c, &b, &a],
            ] {
                let out = ClippingProcessor::consolidate_coplanar(union_many(&order));
                assert_eq!(
                    open_edges(&out),
                    Ok(0),
                    "a three-operand near-coplanar union must come back closed (dz={dz})"
                );
            }
        }
    }
}

/// A two-cutter group whose arrangement does not conform, so `subtract_many`
/// reaches its lenient batch and the volume oracle. Captured (exact f32 coords)
/// from the public `ara3d/dental_clinic.ifc` fixture: a gable wall already
/// holding three windows, a stepped cutter trimming its sloped end, and a
/// window cutter sharing the `z = 7.21` plane with it.
fn lenient_gable_group() -> (Mesh, Mesh, Mesh) {
    let wall = mesh_of(
        &[
            [0.11300659, 35.7639, 9.707001], [0.11300659, 50.47, 9.707001],
            [0.11300659, 50.47, 4.5700073], [0.11300659, 33.630005, 4.5700073],
            [0.11300659, 33.630005, 7.2100067], [0.11300659, 35.322647, 9.190659],
            [0.11300659, 49.10556, 7.2100067], [0.11300659, 48.10556, 7.2100067],
            [0.11300659, 48.10556, 5.475006], [0.11300659, 49.10556, 5.475006],
            [0.11300659, 44.085556, 7.2100067], [0.11300659, 43.085556, 7.2100067],
            [0.11300659, 43.085556, 5.475006], [0.11300659, 44.085556, 5.475006],
            [0.11300659, 42.164444, 7.2100067], [0.11300659, 41.164444, 7.2100067],
            [0.11300659, 41.164444, 5.475006], [0.11300659, 42.164444, 5.475006],
            [0.38000488, 49.10556, 5.475006], [0.38000488, 49.10556, 7.2100067],
            [0.38000488, 44.085556, 5.475006], [0.38000488, 44.085556, 7.2100067],
            [0.38000488, 42.164444, 5.475006], [0.38000488, 42.164444, 7.2100067],
            [0.38000488, 33.630005, 4.5700073], [0.38000488, 33.630005, 7.2100067],
            [0.38000488, 35.7639, 9.707001], [0.38000488, 48.10556, 7.2100067],
            [0.38000488, 43.085556, 7.2100067], [0.38000488, 41.164444, 7.2100067],
            [0.38000488, 50.47, 4.5700073], [0.38000488, 41.164444, 5.475006],
            [0.38000488, 43.085556, 5.475006], [0.38000488, 48.10556, 5.475006],
            [0.38000488, 50.47, 9.707001],
        ],
        &[
            [3, 4, 5], [1, 2, 6], [1, 6, 7], [2, 8, 9], [6, 2, 9], [1, 7, 10], [7, 8, 10],
            [0, 1, 11], [1, 10, 11], [2, 12, 13], [8, 2, 13], [10, 8, 13], [0, 11, 14],
            [11, 12, 14], [0, 14, 15], [5, 0, 15], [3, 5, 16], [5, 15, 16], [2, 3, 17],
            [3, 16, 17], [12, 2, 17], [14, 12, 17], [9, 18, 19], [9, 19, 6], [13, 20, 21],
            [13, 21, 10], [17, 22, 23], [17, 23, 14], [3, 24, 25], [3, 25, 4], [25, 5, 4],
            [5, 25, 26], [0, 5, 26], [6, 19, 27], [6, 27, 7], [10, 21, 28], [10, 28, 11],
            [14, 23, 29], [14, 29, 15], [2, 30, 24], [2, 24, 3], [16, 31, 22], [16, 22, 17],
            [12, 32, 20], [12, 20, 13], [8, 33, 18], [8, 18, 9], [26, 34, 1], [26, 1, 0],
            [15, 29, 31], [15, 31, 16], [11, 28, 32], [11, 32, 12], [7, 27, 33], [7, 33, 8],
            [1, 34, 30], [1, 30, 2], [30, 18, 33], [30, 34, 19], [34, 27, 19], [18, 30, 19],
            [30, 33, 20], [33, 27, 20], [30, 20, 32], [34, 26, 28], [34, 28, 21], [27, 34, 21],
            [20, 27, 21], [24, 30, 22], [30, 32, 22], [32, 28, 22], [24, 22, 31], [26, 25, 31],
            [25, 24, 31], [26, 31, 29], [26, 29, 23], [28, 26, 23], [22, 28, 23],
        ],
    );
    let notch = mesh_of(
        &[
            [0.032899998, 35.763905, 8.4585], [0.032899998, 35.763905, 9.707],
            [0.032899998, 33.63, 7.21], [0.032899998, 34.7635, 7.21],
            [0.032899998, 34.7635, 8.4585], [0.46010488, 35.763905, 8.4585],
            [0.46010488, 35.763905, 9.707], [0.46010488, 33.63, 7.21],
            [0.46010488, 34.7635, 7.21], [0.46010488, 34.7635, 8.4585],
        ],
        &[
            [0, 4, 1], [1, 4, 2], [2, 4, 3], [5, 6, 9], [6, 7, 9], [7, 8, 9], [0, 1, 6], [0, 6, 5],
            [1, 2, 7], [1, 7, 6], [2, 3, 8], [2, 8, 7], [3, 4, 9], [3, 9, 8], [4, 0, 5], [4, 5, 9],
        ],
    );
    let window = mesh_of(
        &[
            [0.032899998, 36.66444, 7.21], [0.032899998, 35.66444, 7.21],
            [0.032899998, 35.66444, 5.475], [0.032899998, 36.66444, 5.475],
            [0.46010488, 36.66444, 7.21], [0.46010488, 35.66444, 7.21],
            [0.46010488, 35.66444, 5.475], [0.46010488, 36.66444, 5.475],
        ],
        &[
            [0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7], [0, 1, 5], [0, 5, 4],
            [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
        ],
    );
    (wall, notch, window)
}

/// `subtract_many`'s volume oracle on an OPEN host (#4693).
///
/// The oracle accepted a lenient batch when `host − batch` matched Σ|host ∩
/// cutter| within 1 %, with the host summed about its own AABB centre and the
/// batch about ITS own. An open surface's sum moves with the reference point,
/// so when the cut trims the host's bounding box, the flux of a crack the cut
/// never touched lands in the removed volume.
///
/// Fixture: the non-conforming gable group above, plus a closed 0.1 m rod
/// standing 0.13 m off the wall's +Y end (1.87 m long) and a third cutter that
/// takes its outer metre. That moves the AABB centre 0.5 m in −Y. The open host
/// adds one unpaired triangle inside the wall at `y = 45` (normal ±Y, 0.47 m²),
/// away from every cutter. Both hosts remove the same solid. On origin/main the
/// closed host is cut and the open one reads 5.577 against the oracle's 5.107
/// (6× m³) and falls back as `Nonconforming`. Mutation: read `batch` about
/// `volume_reference(&batch)` again and the open-host assertion fails.
#[test]
fn lenient_batch_on_an_open_host_reads_both_volumes_about_one_point_4693() {
    use super::super::arrangement::{box_mesh, difference_all};
    use crate::router::voids::geom::mesh_signed_volume_about;
    let (wall, notch, window) = lenient_gable_group();
    let rod = tris_to_mesh(&box_mesh([0.2, 50.6, 6.0], [0.3, 52.47, 6.1]));
    let tip = tris_to_mesh(&box_mesh([0.1, 51.47, 5.9], [0.4, 53.0, 6.2]));
    let mut closed = wall.clone();
    closed.merge(&rod);
    let mut open = closed.clone();
    open.merge(&mesh_of(&[[0.15, 45.0, 4.8], [0.35, 45.0, 4.8], [0.25, 45.0, 9.5]], &[[0, 1, 2]]));
    let cutters = [&notch, &window, &tip];

    // Guard: the group must reach the lenient path, or nothing here reads the oracle.
    let h = orient_outward(mesh_to_tris(&open));
    let comps: Vec<Vec<Tri>> = cutters
        .iter()
        .map(|m| {
            let mut c = mesh_to_tris(m);
            promote_cutter_verts_onto_host_faces(&mut c, &h);
            orient_outward(c)
        })
        .collect();
    let refs: Vec<&[Tri]> = comps.iter().map(|c| c.as_slice()).collect();
    assert!(difference_all(&h, &refs).is_none(), "the group must not conform");

    // Host and cut read about one point on the unpaired triangle's plane, so
    // the triangle adds nothing to either reading, snapped or not.
    let removed = |host: &Mesh, what: &str| {
        let cut = expect_cut(subtract_many(host, &cutters), what);
        let o = [0.25, 45.0, 7.0];
        mesh_signed_volume_about(host, &o) - mesh_signed_volume_about(&cut, &o)
    };
    let closed_removed = removed(&closed, "closed host");
    let open_removed = removed(&open, "open host: the oracle read the crack as removed volume");
    assert!(
        (open_removed - closed_removed).abs() < 1e-4,
        "open host removed {open_removed} m³, closed host {closed_removed} m³"
    );
}