BREP_kernel 0.3.1

A boundary representation (BREP) geometry kernel for building CAD applications.
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
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(
        "../test-data/cache/abc/files/abc_0001_step_v00/00011516/00011516_547fd4f9ecd74f86ba27d0c7_step_032.step",
    );
    if !path.exists() {
        return; // The large validation corpus is optional in kernel-only checkouts.
    }
    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",
        );
    }
}

/// ABC 00000039's torus bands each contain two real full-wrap rims plus a
/// collapsed VERTEX_LOOP. Mass properties and tessellation discard that
/// zero-area puncture before selecting the band; classification must use
/// the same region or boolean/imprint probes reject points on the mesh.
#[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();
    // Midpoint of the vertical edge shared by the +X and +Y faces: a
    // single face normal is ambiguous here, the classification normal
    // must be the normalized sum of the two adjacent face normals.
    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:?}"
    );
}

/// Regression for the tangency-station probe-ray parity loss (mixed
/// curved-wall boss, third-fillet EXACT cutter):
/// a probe point sitting exactly on the station plane through the
/// wall×blend TANGENCY LINE at (10, y*, z), y* = 5 − √(1.4² − 0.4²),
/// probes with a ray that leaves the prism through the hairline sliver
/// next to the tangency, so its only true boundary crossing is the
/// small radius-0.4 blend tube — which the root finder used to seed
/// from full-ray-length sample segments and silently miss (parity 0 →
/// false Out).  The solid is the post-fillet cross-section extruded
/// directly: prism + semicircular boss + the exact tangent fillet arc.
#[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();
    // Fillet arc: center (10.4, y*), radius 0.4, tangent to the wall
    // x=10 at (10, y*) and to the boss (radius 1 about (10,5)) at
    // angle acos(2/7).
    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");
    // Interior point ON the tangency station plane, just inside the wall
    // and just under the cap (the exact-cutter bulkhead's test point).
    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"
    );
    // The blend tube's void, same station: must stay Out.
    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
    );
}

/// Regression for the SEAM-STRADDLING containment misread (fixture 16:
/// face 519 of STEP 00000312, trial 174's operand B, extracted alone).
/// The face is closed in u and one of its five loops crosses the seam, so
/// its flat polygon is torn and plain even-odd read a genuine interior
/// point as Outside — `process_curve`'s midpoint gate then dropped a real
/// section sub-segment and the boolean failed with non-integral genus.
/// `wrapped_horizon_point_in_face` unwraps the loops into the covering
/// plane and even-odds the query's period images.
///
/// NO in-test env tamper: fixture 13's helmet face is itself in the
/// affected class, so toggling `BREP_HORIZON_CONTAINMENT` mid-run races
/// its concurrently-running imprint test (observed flake). Tamper-verify
/// MANUALLY instead: `BREP_HORIZON_CONTAINMENT=0 cargo test --release
/// seam_straddling_loop` must FAIL the Inside assert (it reproduces the
/// flat even-odd misread) — mirroring the conform-guard test's pattern.
#[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"
    );
}

/// Regression for the CROSS-FRAME parity combining in
/// `wrapped_horizon_point_in_face` (t181/00000312#p7, the pool's RANK-1
/// wrapped-horizon class — which measurement showed is NOT a winding
/// horizon at all: face 519's loop 4 is a SEAM-STRADDLING HOLE, net
/// winding 0, unwrapping to u∈[−0.039, 0.039]). The old combining tested
/// all loops at each single period image and declared Inside if ANY image
/// produced an odd crossing count; a query INSIDE the hole at u≈0.986 is
/// contained by the outer rectangle at shift 0 and by the unwrapped hole
/// at shift −P — each image alone counts 1 (odd ⇒ Inside), the true even
/// count is never assembled, and fragment selection kept a PHANTOM
/// half-hole fragment (same-sense coedges at assembly). Per-loop parity
/// summed across images restores the covering-space even-odd.
///
/// NO in-test env tamper (same rationale as the fixture-16 test above:
/// concurrent tests race the process env). Tamper-verify MANUALLY:
/// `BREP_HORIZON_CROSS_FRAME=0 cargo test --release
/// seam_straddling_hole_interior` must FAIL the Outside assert (the
/// legacy per-image combining reads the in-hole point as 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");
    // Inside hole loop 4 (ground truth: winding −1 at the u−1 period
    // image of the loop's own unwrapped pcurve polygon) — off-material.
    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"
    );
    // Material control at the same azimuth, below the hole's v-band.
    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"
    );
}

/// FALSIFICATION PIN for the doubly-periodic "seam-image retry" (fixture
/// 17: trial 35's operand B, STEP 00000608 p2; patch preserved in
/// scripts/seam_image_retry.falsified.patch). The retry reclassified an
/// Outside verdict near a closed direction's domain edge by testing the
/// point's ±period images — but the near-rim point below belongs to
/// NEIGHBOR face 40 (same torus surface, adjacent trim), proven by
/// point_claim_probe: projecting its 3D image onto face 40 lands Inside at
/// dist 2.2e-5, while face 27's material band is v∈[0,0.25] (its plain
/// verdicts are self-consistent). The retry's Inside came from feeding
/// OUT-OF-DOMAIN uv (v=-0.001) into branch paths that assume in-domain
/// queries; downstream it handed face 27 two dangling illegitimate cut
/// chains and zeroed its fragments (keep-whole regressed to dropped).
/// These asserts pin the correct ownership on both faces so any future
/// classification-layer change that flips either one is flagged here.
#[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");
    // The same 3D point (8.112727, 3.202972, 13.545175), in each face's uv.
    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"
    );
    // Face 27's own material band stays intact.
    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"
    );
}

/// RAY-PARITY regression (fixture 18: STEP 00000231 p4, a valid closed
/// 74-face solid). The first clean classification ray from the pinned
/// point pierces a thin curved flange TWICE ~3 mm apart, the root finder
/// returns only one crossing, and single-ray parity said In for a point
/// ~2 mm OUTSIDE the material (`point_owner_probe` finds no face within
/// 2 mm). Ray-agreement voting (two clean directions must agree) pins the
/// correct Out. This false-In corrupted the `solid_overlap_probe`
/// adjudication of an entire pool-residual class before it was caught.
/// NO in-test env tamper; manual tamper: `BREP_CLASSIFY_RAY_AGREE=0
/// cargo test --release ray_parity_thin_flange` must FAIL (restores the
/// single-ray In).
#[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"
    );
    // Control: a point far outside everything stays Out, and a point on
    // the far interior side of the pinned flange region classifies
    // consistently across the agreement rays (no error).
    assert_eq!(
        classify_point(Vec3::new(5000.0, 5000.0, 5000.0), &solid, 1e-7)
            .unwrap()
            .class,
        PointClass::Out
    );
}

/// SINGLE-LOOP seam-straddler containment (t91's face 675, in fixture
/// 18's part): ONE in-domain loop hops the u-seam twice and trims a
/// sliver strip AT the seam (u∈[−0.0185, 0.0185] unwrapped). The old
/// `loops.len() <= 2` gate kept `wrapped_horizon_point_in_face` out and
/// plain even-odd read the hopped polygon INVERTED — strip interior
/// Outside, far azimuths Inside — minting bogus far-azimuth section
/// pieces and dropping the real strip region (zero fragments,
/// no-boundary-faces). These pins hold the corrected orientation.
/// NO in-test env tamper; manual tamper: `BREP_HORIZON_SINGLE_LOOP=0
/// cargo test --release single_loop_seam_straddler` must FAIL.
#[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"
        );
    }
}

/// BAND-CONTAINMENT pins (case 08:10-genus, corpus 21): three imported
/// band faces in seam representations outside every previously landed
/// lane read Outside across their material interior, crumbling the
/// sphere×band sections to sub-mm bits (0 pieces), blocking the band
/// seam insertion, and stranding 28 one-use edges (union S=2 genus=8).
/// Two lanes fix the containment:
///   - faces 1604/1616 (doubly-periodic bands between a flat rim at
///     v=0.5 — NOT a cross-domain extreme — and a scalloped full-wrap
///     cut at v=0.75 dipping to ~0.678): the IN-DOMAIN-RIM variant of
///     `analyze_doubly_periodic_seam_band`;
///   - face 1513 (singly-periodic strip whose full-wrap rims are
///     covering-plane pcurves running out of the u-domain by up to one
///     period): `covering_rim_strip_point_in_face`, a sampled per-u hull
///     (the upper rim is flat at v=1 only on parts of u, dipping to
///     v=0.745 — a v-between-extremes rule is provably wrong on it).
/// The Outside pins in the scallop-dip zones prove the lanes are NOT the
/// force-Inside diagnostic hack they replaced (the hack classified those
/// zones Inside).
/// NO in-test env tamper; manual tampers: `BREP_BAND_INDOMAIN_RIM=0
/// cargo test --release band_rim_containment` and
/// `BREP_COVERING_RIM_STRIP=0 cargo test --release band_rim_containment`
/// must each FAIL.
#[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}"))
    };
    // Face 1604: material band v∈[0.5, cut(u)].
    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)"
        );
    }
    // Face 1616: same class, mirrored loop order.
    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"
    );
    // Face 1513: strip between the flat v=0 rim and the scalloped v≈1 rim.
    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"
        );
    }
}