BREP_kernel 0.16.0

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
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
use super::*;

// ======================================================================
// §6.9.7 vertex ("star") blend — convex trihedral corner.
//
// When three edges meeting at a convex vertex are filleted, the rolling
// ball tangent to all three faces sits at ONE position and its sphere is
// the vertex patch (Golovanov §6.9.7). The patch is a spherical octant:
// with the sphere centered at C = P − r·Σ mᵢ (mᵢ = outward face normals),
// its three corners are the tangent points Tᵢ = C + r·mᵢ and its three
// boundary arcs are the tangent circles sphere∩cylinderᵢ, which lie in
// the coordinate planes through C (arc T₁T₂ = sphere∩(edge-3 cylinder),
// in the plane ⟂ m₃, etc.). Built as a sub-rectangle u∈[0,¼], v∈[½,1] of
// a sphere revolution with polar axis m₃ and x-axis m₁: the three arcs
// are exact iso-parameter curves (two meridians + the equator) and the
// v=1 pole is a degenerate edge at T₃. No booleans — the tangent
// surfaces are never intersected, only constructed and sewn.

/// Build ONLY the spherical-octant `FaceRecord` (plus its degenerate pole
/// edge) for a vertex blend, reusing the three EXISTING tangent vertices
/// `t_ids` and the three FRESH tangent-circle arc edges in `arcs`
/// (`(edge_id, start_vertex, end_vertex)`).  The loop, pcurves and
/// same_sense are exactly the spherical-octant construction described above;
/// each arc coedge's
/// `forward` is derived from the stored edge orientation so the octant use
/// is opposite to the fillet use (shared edge, two coedges, opposite sense).
pub(super) fn build_octant_face(
    center: Vec3,
    normals: [Vec3; 3],
    radius: f64,
    t_ids: [u64; 3],
    t_pts: [Vec3; 3],
    arcs: &[(u64, u64, u64)],
    name: Option<&str>,
    next_id: &mut dyn FnMut() -> u64,
) -> Result<(FaceRecord, EdgeRecord), KernelRefusal> {
    let [m1, m2, m3] = normals;
    if m1.cross(m2).dot(m3) < 0.5 {
        return Err(KernelRefusal::internal(
            KernelStage::Fragment,
            "octant_frame",
            "round_convex_corner: octant normals are not right-handed",
        ));
    }
    let half = std::f64::consts::FRAC_PI_2;
    let meridian = crate::make_arc(center, m1, m3, radius, -half, half)
        .or_refuse(KernelStage::Fragment, "make_arc")?;
    let surface = crate::make_revolution(center, m3, &meridian, std::f64::consts::TAU)
        .or_refuse(KernelStage::Fragment, "make_revolution")?;

    let [t1, t2, t3] = t_ids;
    let e_pole = next_id();
    let pole_edge = EdgeRecord {
        id: e_pole,
        curve: crate::make_line(t_pts[2], t_pts[2]).or_refuse(KernelStage::Fragment, "make_line")?,
        t0: 0.0,
        t1: 1.0,
        start_vertex_id: t3,
        end_vertex_id: t3,
        degenerate: true,
        name: None,
    };

    // For a needed traversal `from -> to`, locate the fresh arc joining that
    // pair and report whether its stored orientation already runs that way.
    let find = |from: u64, to: u64| -> Result<(u64, bool), KernelRefusal> {
        for &(eid, s, e) in arcs {
            if s == from && e == to {
                return Ok((eid, true));
            }
            if s == to && e == from {
                return Ok((eid, false));
            }
        }
        Err(KernelRefusal::internal(
            KernelStage::Fragment,
            "tangent_arc",
            format!("round_convex_corner: no tangent arc between vertices {from} and {to}"),
        ))
    };
    let (e12, f12) = find(t1, t2)?; // equator T1->T2  (arc ⟂ m3)
    let (e23, f23) = find(t2, t3)?; // meridian u=0.25 T2->T3 (arc ⟂ m1)
    let (e31, f31) = find(t3, t1)?; // meridian u=0    T3->T1 (arc ⟂ m2)

    let pl = |u0, v0, u1, v1| {
        crate::sweep_topology::parameter_line(u0, v0, u1, v1)
            .or_refuse(KernelStage::Fragment, "parameter_line")
    };
    let loop_id = next_id();
    let coedges = vec![
        CoedgeRecord {
            id: next_id(),
            edge_id: e12,
            forward: f12,
            pcurve: pl(0.0, 0.5, 0.25, 0.5)?,
        },
        CoedgeRecord {
            id: next_id(),
            edge_id: e23,
            forward: f23,
            pcurve: pl(0.25, 0.5, 0.25, 1.0)?,
        },
        CoedgeRecord {
            id: next_id(),
            edge_id: e_pole,
            forward: true,
            pcurve: pl(0.25, 1.0, 0.0, 1.0)?,
        },
        CoedgeRecord {
            id: next_id(),
            edge_id: e31,
            forward: f31,
            pcurve: pl(0.0, 1.0, 0.0, 0.5)?,
        },
    ];

    let mid = surface
        .evaluate(0.125, 0.75)
        .or_refuse(KernelStage::Fragment, "evaluate")?;
    let surface_normal = surface
        .normal(0.125, 0.75)
        .or_refuse(KernelStage::Fragment, "normal")?;
    let outward = mid
        .sub(center)
        .normalized()
        .or_refuse(KernelStage::Fragment, "normalized")?;
    let same_sense = surface_normal.dot(outward) > 0.0;

    let face = FaceRecord {
        id: next_id(),
        surface,
        same_sense,
        loops: vec![LoopRecord {
            id: loop_id,
            coedges,
        }],
        name: name.map(|value| value.to_string()),
    };
    Ok((face, pole_edge))
}

/// General convex-corner vertex patch for a NON-orthogonal trihedral corner:
/// a spherical triangle on the ball (`center`, `radius`) bounded by the three
/// FRESH tangent-circle arcs in `arc_edges`.  Unlike [`build_octant_face`],
/// the three boundary arcs are great circles that are NOT iso-parameter curves
/// of any single revolution parameterization, so each coedge pcurve is FITTED
/// by projecting samples of its arc onto the sphere.  The revolution frame is
/// chosen to keep the whole patch OFF the u-seam and OFF both poles.
///
/// `outward` says which way the material lies: a CONVEX corner's ball sits
/// inside the material and the patch faces away from the centre; a CONCAVE
/// corner's ball sits in the air and the patch faces toward it.
pub(in crate::blend) fn build_general_corner_patch(
    center: Vec3,
    radius: f64,
    normals: &[Vec3],
    arc_edges: &[EdgeRecord],
    outward: bool,
    name: Option<&str>,
    next_id: &mut dyn FnMut() -> u64,
) -> Result<FaceRecord, KernelRefusal> {
    use std::f64::consts::{FRAC_PI_2, PI, TAU};
    // BREP_PROFILE split of one corner patch (the box fillet's eight pay
    // `timing.surgery.closures_ms`): the frame search, the sphere, each
    // arc's track fit, the sense read.
    let _patch_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch_ms");
    let frame_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch.frame_ms");
    let n_faces = normals.len();
    if n_faces < 3 || arc_edges.len() != n_faces {
        return Err(KernelRefusal::input(
            KernelStage::Collect,
            "arc_count",
            format!(
                "build_general_corner_patch: expected N≥3 faces with one arc each, \
             found {n_faces} faces and {} arcs",
                arc_edges.len()
            ),
        ));
    }
    // Patch centre direction (radially outward through the middle of the
    // spherical N-gon); every tangent point Tᵢ = C + r·nᵢ lies within 90° of
    // it, so a seam placed opposite pc keeps the whole patch off u=0.
    let pc = normals
        .iter()
        .fold(Vec3::default(), |acc, n| acc.add(*n))
        .normalized()
        .or_refuse(KernelStage::Fragment, "normalized")?;

    // Revolution frame: polar axis ⟂ pc so the patch straddles the equator
    // (away from both poles).  Among all such axes pick the one that keeps the
    // N tangent points AND the great-circle arc midpoints (normalize(nᵢ+nⱼ))
    // nearest the equator — minimizing the worst |d·polar|.  x-axis = -pc puts
    // the u=0 seam on the far side (patch lands near u≈0.5).
    let e0 = pc
        .perpendicular()
        .or_refuse(KernelStage::Fragment, "perpendicular")?;
    let e1 = pc
        .cross(e0)
        .normalized()
        .or_refuse(KernelStage::Fragment, "normalized")?;
    let mut crit: Vec<Vec3> = normals.to_vec();
    for i in 0..n_faces {
        for j in (i + 1)..n_faces {
            if let Ok(mid) = normals[i].add(normals[j]).normalized() {
                crit.push(mid);
            }
        }
    }
    let mut best_phi = 0.0f64;
    let mut best_max = f64::INFINITY;
    for k in 0..360 {
        let phi = PI * k as f64 / 360.0;
        let polar = e0.scale(phi.cos()).add(e1.scale(phi.sin()));
        let worst = crit
            .iter()
            .fold(0.0f64, |acc, d| acc.max(d.dot(polar).abs()));
        if worst < best_max {
            best_max = worst;
            best_phi = phi;
        }
    }
    let polar = e0
        .scale(best_phi.cos())
        .add(e1.scale(best_phi.sin()))
        .normalized()
        .or_refuse(KernelStage::Fragment, "normalized")?;
    frame_timing.stop();
    let surface_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch.surface_ms");
    let x_axis = pc.scale(-1.0);
    let meridian = crate::make_arc(center, x_axis, polar, radius, -FRAC_PI_2, FRAC_PI_2)
        .or_refuse(KernelStage::Fragment, "make_arc")?;
    let surface = crate::make_revolution(center, polar, &meridian, TAU)
        .or_refuse(KernelStage::Fragment, "make_revolution")?;
    surface_timing.stop();

    // Chain the three arcs into a closed loop traversed OPPOSITE to the fillet
    // use: each arc is stored start->end and used forward=true by its fillet,
    // so the patch reuses it forward=false (end->start).  Following reversed
    // arcs, each successor's stored end meets the current arc's stored start.
    let mut order = vec![0usize];
    let mut used = vec![false; n_faces];
    used[0] = true;
    let mut chain_vertex = arc_edges[0].start_vertex_id;
    for _ in 0..(n_faces - 1) {
        let next = (0..n_faces)
            .find(|&j| !used[j] && arc_edges[j].end_vertex_id == chain_vertex)
            .ok_or_else(|| {
                KernelRefusal::internal(
                    KernelStage::Fragment,
                    "arc_chain",
                    "build_general_corner_patch: tangent arcs do not form a closed N-gon",
                )
            })?;
        used[next] = true;
        order.push(next);
        chain_vertex = arc_edges[next].start_vertex_id;
    }
    if chain_vertex != arc_edges[0].end_vertex_id {
        return Err(KernelRefusal::internal(
            KernelStage::Fragment,
            "arc_loop",
            "build_general_corner_patch: tangent arc loop is not closed",
        ));
    }

    // Each coedge's pcurve is the arc's track on the sphere, fitted by the one
    // verified fitter (`track_fit`): broken at the revolution's knot lines,
    // which every arc of an octant-sized patch crosses, and held to the
    // refinement floor at midpoints it did not interpolate. It used to be one
    // cubic through 33 uniform samples: exact at those samples and 1.3e-5 to
    // 1.9e-5 off in uv between them, peaking where the arc crosses a knot line,
    // which left the patch's trims 5.3e-7·r² of vector area off their arcs on a
    // 10-cube star fillet and its volume +1.09e-4 at r = 9 (measured
    // 2026-09-17). Each arc is walked in the coedge's traversal direction
    // (forward = false: fraction 0 is its end vertex).
    const PATCH_TRACK_SAMPLES: usize = 512;
    const PATCH_TRACK_SAMPLES_LIMIT: usize = 4096;
    let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
    let mut coedges = Vec::with_capacity(n_faces);
    for &ai in &order {
        let track_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch.track_ms");
        let edge = &arc_edges[ai];
        let at = |fraction: f64| edge.curve.evaluate(edge.t1 - (edge.t1 - edge.t0) * fraction);
        let breaks = super::super::track_fit::curve_breaks(&edge.curve, edge.t0, edge.t1, false);
        let fit = super::super::track_fit::fit_curve_track(
            &surface,
            &at,
            &breaks,
            floor,
            PATCH_TRACK_SAMPLES,
            PATCH_TRACK_SAMPLES_LIMIT,
        )?;
        if std::env::var("BREP_DEBUG_TRACK_FIT").is_ok() {
            eprintln!(
                "TRACK_FIT corner patch arc {}: miss {:.3e} at {} samples",
                edge.id, fit.miss, fit.samples
            );
        }
        if !fit.on_floor {
            return Err(KernelRefusal::non_convergence(
                KernelStage::Refine,
                "patch_track",
                format!(
                    "{} the corner patch's pcurve along arc {} misses its arc by {:.3e} at {} \
                 samples, against a floor of {floor:.1e}",
                    crate::blend::PCURVE_OFF_FLOOR,
                    edge.id,
                    fit.miss,
                    fit.samples
                ),
            ));
        }
        coedges.push(CoedgeRecord {
            id: next_id(),
            edge_id: edge.id,
            forward: false,
            pcurve: fit.curve,
        });
        track_timing.stop();
    }

    // same_sense so the face normal points radially away from C for a convex
    // corner, toward it for a concave one.
    let _sense_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch.sense_ms");
    let centre_projection =
        crate::project_point_to_surface(&surface, center.add(pc.scale(radius)))
            .or_refuse(KernelStage::Fragment, "project")?;
    let surface_normal = surface
        .normal(centre_projection.u, centre_projection.v)
        .or_refuse(KernelStage::Fragment, "normal")?;
    let same_sense = (surface_normal.dot(pc) > 0.0) == outward;

    let loop_id = next_id();
    Ok(FaceRecord {
        id: next_id(),
        surface,
        same_sense,
        loops: vec![LoopRecord {
            id: loop_id,
            coedges,
        }],
        name: name.map(|value| value.to_string()),
    })
}

// ----------------------------------------------------------------------
// The exact octant for an orthogonal corner (the box fillet's corner
// closures). On a corner whose three mate normals are mutually orthogonal
// the three tangent arcs are iso-parameter lines of the sphere revolved
// about one normal with another as its x-axis — the equator and two
// meridians — so the patch's pcurves are exact parameter lines and nothing
// is fitted ([`build_octant_face`], which `round_convex_corner` already
// uses). The general patch fits each arc's track on a sphere framed away
// from its poles (`fit_curve_track`, 512 dense samples), which on the box
// fillet is 28.1 of the surgery's 28.4 ms of closures (measured 2026-10-12:
// the fit 19.1, the projection 9.2; a coarser dense grid moves the body's
// bytes at 256 samples already and verifies the same rungs against fewer
// midpoints). The octant's pole is a degenerate edge the caller must push
// into the solid, hence the pair returned. Behind `BREP_BLEND_EXACT_OCTANT`
// (default on, see `exact_octant_on`); it moves the box pin's bytes, whose
// fitted road the byte pins hold with `historical_corner_for_test`.
// ----------------------------------------------------------------------

/// `BREP_BLEND_EXACT_OCTANT=1`/`on` turns the exact octant on, `=0`/`off`
/// off; `set_exact_octant_for_test` forces it on a test's own thread. Default
/// ON since lane O's promotion (2026-10-28): lane N fixed its two census
/// regressions (a concave corner's sense, the delete-face readers' pole), and
/// lane O taught thicken and the offset shell's joint lane to read a pole
/// vertex that is also a corner of real edges; the pinched shells' edge-use
/// reads skip a degenerate edge as `validate` does.
const EXACT_OCTANT_DEFAULT: bool = true;

pub(in crate::blend) fn exact_octant_on() -> bool {
    switch!(live "BREP_BLEND_EXACT_OCTANT", EXACT_OCTANT_DEFAULT)
}


/// [`build_general_corner_patch`], taking the exact octant first when the
/// switch is on and the corner permits it: three mutually orthogonal normals
/// (to 1e-12) and three arcs whose ends sit on the tangent points
/// `C + r·nₖ` (to 1e-9·(1 + r)) in one consistent cycle.  Returns the face
/// and, for the octant, its degenerate pole edge, which the caller pushes
/// into the solid's edges.  Every other corner, and the switch off, is the
/// fitted patch with `None`, byte for byte.
pub(in crate::blend) fn build_corner_patch_exact(
    center: Vec3,
    radius: f64,
    normals: &[Vec3],
    arc_edges: &[EdgeRecord],
    outward: bool,
    name: Option<&str>,
    next_id: &mut dyn FnMut() -> u64,
) -> Result<(FaceRecord, Option<EdgeRecord>), KernelRefusal> {
    if exact_octant_on() {
        if let Some((face, pole)) = try_exact_octant(center, radius, normals, arc_edges, outward, name, next_id)? {
            return Ok((face, Some(pole)));
        }
    }
    Ok((build_general_corner_patch(center, radius, normals, arc_edges, outward, name, next_id)?, None))
}

/// The exact octant when the corner permits it (see
/// [`build_corner_patch_exact`]); `None` says to fit instead.
fn try_exact_octant(
    center: Vec3,
    radius: f64,
    normals: &[Vec3],
    arc_edges: &[EdgeRecord],
    outward: bool,
    name: Option<&str>,
    next_id: &mut dyn FnMut() -> u64,
) -> Result<Option<(FaceRecord, EdgeRecord)>, KernelRefusal> {
    if normals.len() != 3 || arc_edges.len() != 3 {
        return Ok(None);
    }
    let orthogonal = (0..3).all(|i| ((i + 1)..3).all(|j| normals[i].dot(normals[j]).abs() <= 1e-12));
    if !orthogonal || normals.iter().any(|n| (n.length() - 1.0).abs() > 1e-12) {
        return Ok(None);
    }
    // Right-handed labelling: swap two normals when the frame is left-handed.
    let mut m = [normals[0], normals[1], normals[2]];
    if m[0].cross(m[1]).dot(m[2]) < 0.0 {
        m.swap(1, 2);
    }
    let ideal: [Vec3; 3] = [center.add(m[0].scale(radius)), center.add(m[1].scale(radius)), center.add(m[2].scale(radius))];
    let near = 1e-9 * (1.0 + radius);
    let which = |point: Vec3| (0..3).find(|&k| point.sub(ideal[k]).length() <= near);
    // Each arc's ends on two distinct tangent points, the three arcs covering
    // the three pairs, and the stored directions one consistent cycle (the
    // fillets use every arc forward, so the patch must walk each backward).
    let mut triples: Vec<(u64, u64, u64)> = Vec::with_capacity(3);
    let mut t_ids = [None::<u64>; 3];
    let mut labels: Vec<(usize, usize)> = Vec::with_capacity(3);
    for edge in arc_edges {
        let start = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Fragment, "evaluate")?;
        let end = edge.curve.evaluate(edge.t1).or_refuse(KernelStage::Fragment, "evaluate")?;
        let (Some(ks), Some(ke)) = (which(start), which(end)) else { return Ok(None) };
        if ks == ke {
            return Ok(None);
        }
        for (k, id) in [(ks, edge.start_vertex_id), (ke, edge.end_vertex_id)] {
            match t_ids[k] {
                None => t_ids[k] = Some(id),
                Some(known) if known == id => {}
                Some(_) => return Ok(None),
            }
        }
        triples.push((edge.id, edge.start_vertex_id, edge.end_vertex_id));
        labels.push((ks, ke));
    }
    let [Some(t1), Some(t2), Some(t3)] = t_ids else { return Ok(None) };
    let mut pairs: Vec<(usize, usize)> = labels.iter().map(|&(a, b)| (a.min(b), a.max(b))).collect();
    pairs.sort_unstable();
    if pairs != vec![(0, 1), (0, 2), (1, 2)] {
        return Ok(None);
    }
    // A consistent cycle: every label appears once as a start and once as an end.
    let starts: Vec<usize> = labels.iter().map(|&(a, _)| a).collect();
    if !(0..3).all(|k| starts.iter().filter(|&&a| a == k).count() == 1) {
        return Ok(None);
    }
    let (mut face, pole) = build_octant_face(center, m, radius, [t1, t2, t3], ideal, &triples, name, next_id)?;
    // The octant walks T1 → T2 → T3 → T1, counter-clockwise in uv about the
    // revolution's own normal, which points away from the centre: a loop the
    // kernel reads as the face's outer loop only on a `same_sense` face, so
    // the built face's normal is radially outward and it can close a CONVEX
    // corner only. The patch must walk every arc against its fillet's use
    // (the stored direction). When the stored cycle runs the octant's way, the
    // loop is walked the other way round — the coedges in reverse order, each
    // pcurve reversed — and is then clockwise in uv: the outer loop of a face
    // whose `same_sense` is false, with its normal radially inward, which is a
    // CONCAVE corner's patch (its ball sits in the air and the body's outward
    // normal points into it). The stored cycle and the material side must
    // agree; a corner where they do not is fitted instead. Until 2026-10-28
    // the reversed loop was also flipped for `!outward`, which left a
    // clockwise loop on a `same_sense` face: mass properties read that as a
    // hole in the whole sphere, and every concave octant added one ball's
    // volume (C5_wedge_nine_pick_runout +0.5236 at r 0.5; the rib-spine
    // census row +1.047 for its two).
    let arcs_forward = face.loops[0].coedges.iter().filter(|coedge| coedge.edge_id != pole.id).map(|coedge| coedge.forward).collect::<Vec<bool>>();
    let reversed = arcs_forward.iter().any(|&forward| forward);
    if reversed && !arcs_forward.iter().all(|&forward| forward) {
        return Ok(None);
    }
    if reversed == outward {
        crate::blend::record_count("count.corner.octant_cycle_disagrees", 1);
        return Ok(None);
    }
    if reversed {
        let mut walked_back = Vec::with_capacity(face.loops[0].coedges.len());
        for coedge in face.loops[0].coedges.iter().rev() {
            walked_back.push(CoedgeRecord {
                id: coedge.id,
                edge_id: coedge.edge_id,
                forward: if coedge.edge_id == pole.id { true } else { !coedge.forward },
                pcurve: coedge.pcurve.reversed().or_refuse(KernelStage::Fragment, "reversed")?,
            });
        }
        face.loops[0].coedges = walked_back;
        face.same_sense = !face.same_sense;
    }
    Ok(Some((face, pole)))
}

/// The CHAMFER counterpart of [`build_general_corner_patch`]: the planar facet
/// a chamfered star closes with.
///
/// §6.11 keeps a chamfered vertex sharp only where the bevels meet in a seam.
/// At a STAR — every edge at the vertex selected — each stripe stops at the
/// station where its own rolling ball IS the corner ball, and there its cross
/// section is the CHORD between the ball's two tangency points rather than the
/// arc between them.  The N chords therefore run tangency point to tangency
/// point around the ball, one per face, and close an N-gon whose vertices all
/// lie on the ball.  For N = 3 that N-gon is a triangle, so it is planar by
/// construction and the facet is the one plane through the three tangency
/// points; the chords lie in it exactly, which is what lets the facet be sewn
/// to the stripes with no intersection anywhere.
///
/// N ≥ 4 is refused: four points on a sphere are not coplanar in general, and
/// the fill that closes a non-planar N-gon is the question
/// `general_star.rs` answers for fillets, not this one.
pub(in crate::blend) fn build_chamfer_corner_facet(
    center: Vec3,
    normals: &[Vec3],
    chord_edges: &[EdgeRecord],
    outward: bool,
    name: Option<&str>,
    next_id: &mut dyn FnMut() -> u64,
) -> Result<FaceRecord, KernelRefusal> {
    let n_faces = normals.len();
    if n_faces < 3 || chord_edges.len() != n_faces {
        return Err(KernelRefusal::input(
            KernelStage::Collect,
            "chord_count",
            format!(
                "blend network: a chamfered star closes with the facet its end chords bound, which \
             needs N≥3 faces carrying one chord each — this vertex has {n_faces} faces and {} \
             sections",
                chord_edges.len()
            ),
        ));
    }
    // Outward direction through the middle of the corner, exactly as the
    // spherical patch uses it: every tangency point lies within 90 degrees of
    // it, so it fixes the facet's sense.
    let pc = normals
        .iter()
        .fold(Vec3::default(), |acc, n| acc.add(*n))
        .normalized()
        .or_refuse(KernelStage::Fragment, "normalized")?;

    // Chain the chords into a closed loop traversed OPPOSITE to the stripes'
    // use, the same walk `build_general_corner_patch` makes over the arcs.
    let mut order = vec![0usize];
    let mut used = vec![false; n_faces];
    used[0] = true;
    let mut chain_vertex = chord_edges[0].start_vertex_id;
    for _ in 0..(n_faces - 1) {
        let next = (0..n_faces)
            .find(|&j| !used[j] && chord_edges[j].end_vertex_id == chain_vertex)
            .ok_or_else(|| {
                KernelRefusal::internal(
                    KernelStage::Fragment,
                    "chord_chain",
                    "blend network: the chamfer facet's sections do not form a closed N-gon",
                )
            })?;
        used[next] = true;
        order.push(next);
        chain_vertex = chord_edges[next].start_vertex_id;
    }
    if chain_vertex != chord_edges[0].end_vertex_id {
        return Err(KernelRefusal::internal(
            KernelStage::Fragment,
            "chord_loop",
            "blend network: the chamfer facet's section loop is not closed",
        ));
    }

    // The plane the N-gon bounds. Each tangency point is `center + r·nᵢ` for
    // the one seated radius, and every face at the corner passes through the
    // corner VERTEX V, so `nᵢ·(V − center) = −r` for all of them: the points
    // all lie on the plane with normal `V − center`, whatever N is. That is an
    // identity for PLANAR supports only — a curved support puts its tangency
    // point somewhere the identity does not reach — so the N-gon's flatness is
    // measured rather than assumed, and a skew one is refused by name.
    let corner_point = |index: usize| -> Result<Vec3, KernelRefusal> {
        let edge = &chord_edges[index];
        edge.curve
            .evaluate(edge.t0)
            .or_refuse(KernelStage::Fragment, "evaluate")
    };
    let points = order
        .iter()
        .map(|&index| corner_point(index))
        .collect::<Result<Vec<_>, KernelRefusal>>()?;
    let [a, b] = [points[0], points[1]];
    let u_dir = b
        .sub(a)
        .normalized()
        .or_refuse(KernelStage::Fragment, "normalized")?;
    // Newell's normal over the chained N-gon: exact for a planar polygon at
    // any N, and for N = 3 exactly the triangle's own `(b−a)×(c−a)`.
    let mut newell = Vec3::default();
    for index in 0..n_faces {
        let from = points[index];
        let to = points[(index + 1) % n_faces];
        newell = newell.add(from.sub(a).cross(to.sub(a)));
    }
    let normal = newell
        .normalized()
        .or_refuse(KernelStage::Fragment, "normalized")?;
    let v_dir = normal
        .cross(u_dir)
        .normalized()
        .or_refuse(KernelStage::Fragment, "normalized")?;
    // The seated radius, which every tangency point shares, sets the bar the
    // N-gon's flatness is held to.
    let radius = points
        .iter()
        .fold(0.0f64, |worst, point| worst.max(point.sub(center).length()));
    let flatness = 1e-9 * (1.0 + radius);
    let skew = points
        .iter()
        .fold(0.0f64, |worst, point| worst.max(point.sub(a).dot(normal).abs()));
    if skew > flatness {
        return Err(KernelRefusal::unsupported(
            KernelStage::Classify,
            "skew_ngon",
            format!(
                "blend network: the {n_faces} chords at a chamfered star bound a SKEW N-gon \
             ({skew:.3e} out of plane against {flatness:.3e}); a non-planar corner fill is not \
             implemented"
            ),
        ));
    }
    // A patch big enough to hold the facet with room to spare, laid out so the
    // facet lands inside the parameter square (the same set-back `sew_cap`
    // uses for its bulkhead).
    let reach = points
        .iter()
        .fold(0.0f64, |worst, point| worst.max(point.sub(a).length()))
        .max(1e-6)
        * 4.0;
    let origin = a.sub(u_dir.scale(reach)).sub(v_dir.scale(reach));
    let plane = crate::make_plane(origin, u_dir, v_dir, 2.0 * reach, 2.0 * reach)
        .or_refuse(KernelStage::Fragment, "make_plane")?;
    let uv_of = |point: Vec3| -> Result<(f64, f64), KernelRefusal> {
        let projection = crate::project_point_to_surface(&plane, point)
            .or_refuse(KernelStage::Fragment, "project")?;
        Ok((projection.u, projection.v))
    };

    // A chord is a straight line and the plane's parameterisation is affine,
    // so each pcurve is the straight uv segment between the chord's ends —
    // exact, with nothing to fit. `forward = false` walks stored end -> start.
    let mut coedges = Vec::with_capacity(n_faces);
    for &index in &order {
        let edge = &chord_edges[index];
        let (u0, v0) = uv_of(
            edge.curve
                .evaluate(edge.t1)
                .or_refuse(KernelStage::Fragment, "evaluate")?,
        )?;
        let (u1, v1) = uv_of(
            edge.curve
                .evaluate(edge.t0)
                .or_refuse(KernelStage::Fragment, "evaluate")?,
        )?;
        coedges.push(CoedgeRecord {
            id: next_id(),
            edge_id: edge.id,
            forward: false,
            pcurve: crate::sweep_topology::parameter_line(u0, v0, u1, v1)
                .or_refuse(KernelStage::Fragment, "parameter_line")?,
        });
    }

    // same_sense so the face normal points away from the ball's centre at a
    // convex corner and toward it at a concave one — the patch's own rule.
    let plane_normal = plane
        .normal(0.5, 0.5)
        .or_refuse(KernelStage::Fragment, "normal")?;
    let same_sense = (plane_normal.dot(pc) > 0.0) == outward;

    let loop_id = next_id();
    Ok(FaceRecord {
        id: next_id(),
        surface: plane,
        same_sense,
        loops: vec![LoopRecord {
            id: loop_id,
            coedges,
        }],
        name: name.map(|value| value.to_string()),
    })
}