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
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
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
use crate::{KernelRefusal, KernelStage};
use super::*;

/// In-segment stations per chain segment, for a wall that does not fold.
pub(in crate::blend) const CHAIN_PER_SEGMENT: usize = 24;

///
/// The ladder climbs the `rung` (24, 48, 96, ...), which used to be every
/// segment's count whatever its length: on the 2026-09-02 collar the two arcs
/// are 20 % and 80 % of the edge, so the long arc's chords were four times
/// the short arc's and every rung's miss sat in the long arc while the short
/// one read three to five decades under the bar. [`ChainBudget::for_chain`]
/// shares the rung's `rung × segments` stations out IN PROPORTION TO ARC
/// LENGTH, with a floor of a quarter of the rung so a short arc keeps a grid
/// of its own; a chain whose segments are equal in length gets `rung` in
/// every segment, exactly the old grid. Behind
/// [`crate::blend::chain_arc_stations_on`]; [`ChainBudget::uniform`] is the
/// old grid, which the switch's off side and the march's own unit tests use.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(in crate::blend) struct ChainBudget {
    /// The rung the ladder climbed to: the per-segment count of the uniform
    /// grid, and what the traces and the typed reports name.
    pub(in crate::blend) rung: usize,
    /// In-segment stations of each segment (positions `0..count`; `count` is
    /// the segment's end station, shared with the next segment's position 0).
    pub(in crate::blend) counts: Vec<usize>,
}

impl ChainBudget {
    /// The old grid: `rung` stations in every one of `segments` segments.
    pub(in crate::blend) fn uniform(rung: usize, segments: usize) -> Self {
        ChainBudget { rung, counts: vec![rung; segments] }
    }

    /// The rung's stations shared out by arc length (the switch on), else
    /// the uniform grid.
    pub(in crate::blend) fn for_chain(segments: &[ChainSegment<'_>], rung: usize) -> Result<Self, KernelRefusal> {
        if !crate::blend::chain_arc_stations_on() || segments.len() < 2 {
            return Ok(Self::uniform(rung, segments.len()));
        }
        let mut lengths = Vec::with_capacity(segments.len());
        for segment in segments {
            let length = crate::edge_arc_length(segment.edge)
                .map_err(|error| KernelRefusal::internal(KernelStage::Refine, "edge_arc_length", error))?;
            if !(length.is_finite() && length > 0.0) {
                return Err(KernelRefusal::input(KernelStage::Collect, "chain_segment_length", format!(
                    "blend chain: edge {} has no arc length to share stations by ({length})",
                    segment.edge.id
                )));
            }
            lengths.push(length);
        }
        Ok(Self::by_lengths(rung, &lengths))
    }

    /// The allocation itself, from the segment lengths: `rung × n` stations
    /// in proportion, each rounded, none under a quarter of the rung (and
    /// never under 2, the middle station's own grid).
    pub(in crate::blend) fn by_lengths(rung: usize, lengths: &[f64]) -> Self {
        let total: f64 = lengths.iter().sum();
        let stations = (rung * lengths.len()) as f64;
        let floor = (rung / 4).max(2);
        let counts = lengths
            .iter()
            .map(|length| ((stations * length / total).round() as usize).max(floor))
            .collect();
        ChainBudget { rung, counts }
    }

    /// This segment's in-segment station count.
    pub(in crate::blend) fn count(&self, segment: usize) -> usize {
        self.counts[segment]
    }

    /// The largest segment count: what the ladder's resource ceiling reads.
    pub(in crate::blend) fn largest(&self) -> usize {
        self.counts.iter().copied().max().unwrap_or(self.rung)
    }

    /// The next rung: every count doubled, so each old interval is halved and
    /// a station is seeded from a neighbour half as far away.
    pub(in crate::blend) fn doubled(&self) -> Self {
        ChainBudget { rung: self.rung * 2, counts: self.counts.iter().map(|count| count * 2).collect() }
    }

    /// For traces: the rung, and the per-segment counts when they differ.
    pub(in crate::blend) fn describe(&self) -> String {
        if self.counts.iter().all(|&count| count == self.rung) {
            format!("{} stations per segment", self.rung)
        } else {
            let counts: Vec<String> = self.counts.iter().map(|count| count.to_string()).collect();
            format!("rung {} ({} stations by arc length)", self.rung, counts.join("/"))
        }
    }
}

/// A ladder that topped out without passing, as its refusal reads it: the
/// best rung's per-segment count and reading, and every rung's reading in the
/// order climbed (lane L, 2026-10-16, lCount_234). The top rung alone misleads:
/// it can reach the station ceiling before any local round, so its reading is
/// the raw first round's, not the closest the ladder came.
pub(super) fn ladder_trend(trend: &[(usize, f64)]) -> (usize, f64, String) {
    let (best_rung, best) = trend
        .iter()
        .copied()
        .fold((0, f64::INFINITY), |best, (rung, reading)| if reading < best.1 { (rung, reading) } else { best });
    let listing: Vec<String> = trend.iter().map(|(rung, reading)| format!("{rung} {reading:.3e}")).collect();
    (best_rung, best, listing.join(", "))
}

/// The TOP of the ladder a folding chain is re-marched up (24, 48, 96, ...).
///
/// A carve traces the crease on the surface the fit produced, so it is sound at
/// any budget, but the wall it carves is only as close to the rolling ball as
/// the fit. So the carve does not pick a budget: it climbs, and stops at the
/// first rung whose fitted rails pass the rolling ball's own contacts, re-solved
/// halfway between stations, to within `intersection_fit`
/// (`chain/closed.rs`). The 2026-09-02 collar stops at the rung the record
/// states. This is only the ceiling, and it is a RESOURCE bound, not an
/// accuracy one: a 768-station segment is a 1540-row control net, and a wall
/// that still misses there is refused by name rather than shipped.
pub(super) const CHAIN_CARVE_MAX_PER_SEGMENT: usize = 768;

/// What a march does when the wall it would build folds through itself.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(in crate::blend) enum FoldPolicy {
    /// Refuse by name, terminally (`blend/fold.rs`).
    Refuse,
    /// Build it anyway, because the caller is about to CARVE the fold out
    /// (`blend/carve.rs`) and has already re-marched at the budget that needs.
    Carve,
}

const CHAIN_OVERSHOOT: usize = 3;

/// One marched chain station, tagged with its segment and its position
/// (which may overshoot the segment for pcurve fitting).
#[derive(Clone)]
pub(in crate::blend) struct ChainSample {
    pub(in crate::blend) segment: usize,
    /// Station position within the segment: 0..CHAIN_PER_SEGMENT are
    /// in-segment; negatives and > CHAIN_PER_SEGMENT are overshoot.
    pub(in crate::blend) position: isize,
    /// Where between `position` and the next grid station a station inserted
    /// by local refinement sits, in (0, 1); 0 for every marched grid station.
    pub(in crate::blend) offset: f64,
    /// The edge parameter the station's section plane was taken at.
    pub(in crate::blend) t: f64,
    pub(in crate::blend) station: Station,
    /// Global chord parameter (filled after the march).
    pub(in crate::blend) parameter: f64,
    /// The rolling ball's EXACT contacts halfway to the next position — solved,
    /// not interpolated — so a fitted row can be measured where an interpolant
    /// is worst. Only a march for a carve pays for them.
    pub(in crate::blend) midpoint: Option<[Vec3; 2]>,
}

/// A chain station whose CONTINUATION solve did not converge: the tangency
/// Newton, seeded from its solved neighbour (after the branch guard's halving
/// retries), found no root at this budget.  A finer rung is a nearer seed, so
/// the closed ladder may climb on it; the refusal itself is kept verbatim and
/// is what a caller receives at the ladder's top or outside the ladder.
pub(in crate::blend) struct ChainStationFailure {
    pub(in crate::blend) segment: usize,
    pub(in crate::blend) position: isize,
    pub(in crate::blend) t: f64,
    pub(in crate::blend) from_position: isize,
    pub(in crate::blend) from_t: f64,
    pub(in crate::blend) seed_uv: [f64; 4],
    pub(in crate::blend) error: KernelRefusal,
}

/// How a chain march stops: a typed station-continuation failure, or any
/// other refusal, which passes through untouched.
pub(in crate::blend) enum ChainMarchError {
    Station(ChainStationFailure),
    /// Local refinement only: an inserted station, continued from its left
    /// neighbour, is not on the branch of the RETAINED right station (the
    /// existing `branch_hop` reading, carried as `error`). The rung cannot be
    /// refined coherently; the caller's ladder decides, as for a coarse rung.
    Incoherent(ChainStationFailure),
    Other(KernelRefusal),
}

impl ChainMarchError {
    /// The refusal exactly as the march produced it.
    pub(in crate::blend) fn into_refusal(self) -> KernelRefusal {
        match self {
            ChainMarchError::Station(failure) | ChainMarchError::Incoherent(failure) => failure.error,
            ChainMarchError::Other(error) => error,
        }
    }
}

impl From<KernelRefusal> for ChainMarchError {
    fn from(error: KernelRefusal) -> Self {
        ChainMarchError::Other(error)
    }
}

/// The tangency Newton's own non-convergence, by its typed slug
/// (`stations::solve_station_centered`); nothing else is a station failure.
fn is_tangency_newton(error: &KernelRefusal) -> bool {
    matches!(&error.class, crate::RefusalClass::NonConvergence { what } if what == "tangency_newton")
}

pub(in crate::blend) fn march_chain(
    segments: &[ChainSegment<'_>],
    radius: f64,
    open: bool,
    budget: &ChainBudget,
    fold: FoldPolicy,
) -> Result<Vec<ChainSample>, KernelRefusal> {
    march_chain_typed(segments, radius, open, budget, fold).map_err(ChainMarchError::into_refusal)
}

/// [`march_chain`] with a station-continuation non-convergence kept typed, so
/// the closed ladder can tell it from every other refusal.
pub(in crate::blend) fn march_chain_typed(
    segments: &[ChainSegment<'_>],
    radius: f64,
    open: bool,
    budget: &ChainBudget,
    fold: FoldPolicy,
) -> Result<Vec<ChainSample>, ChainMarchError> {
    let mut samples = Vec::new();
    for (index, segment) in segments.iter().enumerate() {
        let per_segment = budget.count(index);
        // Signs from this segment's own cross-section seed.
        let (rho1, rho2) = signed_radii(
            segment.edge,
            segment.first.face,
            segment.first.coedge,
            segment.second.face,
            segment.second.coedge,
            radius,
        )?;
        let rho = [rho1, rho2];
        let surface1 = &segment.first.face.surface;
        let surface2 = &segment.second.face.surface;
        let edge = segment.edge;
        let span = edge.t1 - edge.t0;
        let scale = march_model_scale(&edge.curve, edge.t0, edge.t1, radius)?;
        crate::report_scale_migration("blend_march_chain", scale, || {
            edge.curve
                .evaluate(edge.t0)
                .unwrap_or_default()
                .length()
                .max(1.0)
        });
        let chain_t = |position: isize| -> f64 {
            let fraction = position as f64 / per_segment as f64;
            if segment.forward {
                edge.t0 + span * fraction
            } else {
                edge.t1 - span * fraction
            }
        };
        let mid_position = per_segment as isize / 2;
        let seed_mid = {
            let t = chain_t(mid_position);
            let uv1 = edge_uv_on_face(segment.first.coedge, edge, t)?;
            let uv2 = edge_uv_on_face(segment.second.coedge, edge, t)?;
            [uv1[0], uv1[1], uv2[0], uv2[1]]
        };
        // The section plane at `t`, advancing past a stationary open end by the
        // march's own chord (`march_section`).
        let station_step = span.abs() / per_segment as f64;
        let section_at = |t: f64| -> Result<(Vec3, Vec3), KernelRefusal> {
            march_section(edge, t, station_step, "blend chain march")
        };
        let solve_centered = |t: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3), KernelRefusal> {
            let (section_point, tangent) = section_at(t)?;
            let tangent = if segment.forward {
                tangent
            } else {
                tangent.scale(-1.0)
            };
            solve_station_centered(
                surface1,
                surface2,
                rho,
                seed,
                section_point,
                tangent,
                scale,
            )
        };
        let solve_at = |t: f64, seed: [f64; 4]| -> Result<[f64; 4], KernelRefusal> {
            solve_centered(t, seed).map(|(uv, _)| uv)
        };
        let low = -(CHAIN_OVERSHOOT as isize);
        let high = (per_segment + CHAIN_OVERSHOOT) as isize;
        let count = (high - low + 1) as usize;
        let mut solutions = vec![[0.0f64; 4]; count];
        let mut centers = vec![Vec3::default(); count];
        let index_of = |position: isize| (position - low) as usize;
        // Every station but the middle one is the CONTINUATION of the
        // station it is seeded from: an undamped Newton can converge on the
        // far crossing of the section plane, and the guard retries such a
        // hop from a nearer seed (`stations::continue_station`).  The open
        // chain a box notch leaves of a 20° cylinder crossing's loop — the
        // crotch arc and the sliver past the thin cylinder's seam — hopped
        // here at 24 stations a segment (2026-09-26): its first station
        // past the crotch moved 3.06 and 4.10 periods, the centre 6.4.
        let hop = |left: &Continued, right: &Continued| {
            branch_hop([surface1, surface2], edge, &|_| rho, left, right)
        };
        let continue_from = |solutions: &mut [[f64; 4]],
                                 centers: &mut [Vec3],
                                 position: isize,
                                 from: isize|
         -> Result<(), ChainMarchError> {
            let left = Continued {
                t: chain_t(from),
                uv: solutions[index_of(from)],
                center: Some(centers[index_of(from)]),
            };
            let node = continue_station(&solve_centered, &hop, &left, chain_t(position), 0)
                .map_err(|error| {
                    if std::env::var("BREP_BLEND_STATION_TRACE").ok().as_deref() == Some("1") {
                        eprintln!(
                            "blend chain: segment {index} position {position} (t {:.9}) from position {from}: {error}",
                            chain_t(position)
                        );
                    }
                    if is_tangency_newton(&error) {
                        ChainMarchError::Station(ChainStationFailure {
                            segment: index,
                            position,
                            t: chain_t(position),
                            from_position: from,
                            from_t: left.t,
                            seed_uv: left.uv,
                            error,
                        })
                    } else {
                        ChainMarchError::Other(error)
                    }
                })?;
            solutions[index_of(position)] = node.uv;
            centers[index_of(position)] = node.center.expect("a solved station has a centre");
            Ok(())
        };
        // The segment's OWN stations first, out from the middle, and the
        // overshoot past each end only after the fold check below has read
        // them: the overshoot evaluates both carriers past the segment, where
        // a Newton may not converge at all, and a march that stops there
        // would hide a fold its own stations already prove. Each station is
        // seeded by its neighbour either way, so the solutions are the same.
        let segment_end = per_segment as isize;
        {
            let (uv, center) = solve_centered(chain_t(mid_position), seed_mid).map_err(|error| {
                if std::env::var("BREP_BLEND_STATION_TRACE").ok().as_deref() == Some("1") {
                    eprintln!("blend chain: segment {index} middle station: {error}");
                }
                error
            })?;
            solutions[index_of(mid_position)] = uv;
            centers[index_of(mid_position)] = center;
        }
        for position in (0..mid_position).rev() {
            continue_from(&mut solutions, &mut centers, position, position + 1)?;
        }
        for position in mid_position + 1..=segment_end {
            continue_from(&mut solutions, &mut centers, position, position - 1)?;
        }
        // Does the wall this segment would carry FOLD? Measured on the ball
        // centre curve the stations already sit on, by re-solving the tangency
        // system either side of each — the station spacing itself is far too
        // coarse to read a bend the size of the radius (`blend/fold.rs`).
        {
            let probe = |t: f64,
                         seed: [f64; 4]|
             -> Result<([f64; 4], Vec3, Vec3, Vec3), KernelRefusal> {
                let uv = solve_at(t, seed)?;
                let (section_point, tangent) = section_at(t)?;
                let tangent = if segment.forward {
                    tangent
                } else {
                    tangent.scale(-1.0)
                };
                let (_, p1, p2, center) =
                    tangency_residual(surface1, surface2, rho, uv, section_point, tangent)?;
                Ok((uv, p1, p2, center))
            };
            let probed: Vec<(f64, [f64; 4])> = (0..=per_segment as isize)
                .map(|position| (chain_t(position), solutions[index_of(position)]))
                .collect();
            let radius_at = |_: f64| radius.abs();
            if fold == FoldPolicy::Refuse {
                check_wall_fold(
                    &radius_at,
                    span,
                    &probed,
                    &probe,
                    segment.edge,
                    [segment.first.face, segment.second.face],
                    scale,
                )?;
            }
        }
        for position in (low..0).rev() {
            continue_from(&mut solutions, &mut centers, position, position + 1)?;
        }
        for position in segment_end + 1..=high {
            continue_from(&mut solutions, &mut centers, position, position - 1)?;
        }
        // Halfway contacts, for the closed chain's accuracy ladder — every
        // closed chain, not only one being carved: a collar that does not fold
        // is built from the same interpolated rails and can miss its carriers
        // just as far (9.4e-4 off its torus on the 2026-09-02 two-torus collar,
        // built at the first rung with nothing measuring it).
        let mut midpoints: Vec<Option<[Vec3; 2]>> = vec![None; count];
        if !open {
            for position in 0..per_segment as isize {
                let fraction = (position as f64 + 0.5) / per_segment as f64;
                let t = if segment.forward {
                    edge.t0 + span * fraction
                } else {
                    edge.t1 - span * fraction
                };
                // The halfway contact is the CONTINUATION of its left station
                // under the same branch guard every station takes: its first
                // attempt is the plain solve from the left seed it always was,
                // so a contact that converges on the left station's branch is
                // unchanged to the bit. Where that plain solve does not converge
                // (the 20° seam-split crotch, t 0.208 -> 0.25, needs two seed
                // halvings) the failure is a typed station failure, so the
                // closed ladder can climb on it like any station's.
                let left = Continued {
                    t: chain_t(position),
                    uv: solutions[index_of(position)],
                    center: Some(centers[index_of(position)]),
                };
                let uv = continue_station(&solve_centered, &hop, &left, t, 0)
                    .map_err(|error| {
                        if is_tangency_newton(&error) {
                            ChainMarchError::Station(ChainStationFailure {
                                segment: index,
                                position,
                                t,
                                from_position: position,
                                from_t: left.t,
                                seed_uv: left.uv,
                                error,
                            })
                        } else {
                            ChainMarchError::Other(error)
                        }
                    })?
                    .uv;
                let (section_point, tangent) = section_at(t)?;
                let tangent = if segment.forward {
                    tangent
                } else {
                    tangent.scale(-1.0)
                };
                let (_, p1, p2, _) =
                    tangency_residual(surface1, surface2, rho, uv, section_point, tangent)?;
                midpoints[index_of(position)] = Some([p1, p2]);
            }
        }
        for position in low..=high {
            let t = chain_t(position);
            let uv = solutions[index_of(position)];
            let (section_point, tangent) = section_at(t)?;
            let tangent = if segment.forward {
                tangent
            } else {
                tangent.scale(-1.0)
            };
            let (_, p1, p2, center) =
                tangency_residual(surface1, surface2, rho, uv, section_point, tangent)?;
            let n1 = raw_normal(surface1, uv[0], uv[1])?;
            let n2 = raw_normal(surface2, uv[2], uv[3])?;
            let cos_alpha = rho1.signum() * rho2.signum() * n1.dot(n2);
            let weight = ((1.0 + cos_alpha) * 0.5).max(0.0).sqrt();
            if weight <= 1e-6 {
                return Err(KernelRefusal::unsupported(KernelStage::Refine, "tangent_faces", "blend: faces are tangent at a chain station").into());
            }
            let apex = apex_point(p1, n1, p2, n2, center)?;
            samples.push(ChainSample {
                segment: index,
                position,
                offset: 0.0,
                t,
                station: Station {
                    uv1: [uv[0], uv[1]],
                    uv2: [uv[2], uv[3]],
                    p1,
                    p2,
                    center,
                    weight,
                    apex,
                },
                parameter: 0.0,
                midpoint: midpoints[index_of(position)],
            });
        }
    }
    assign_chain_parameters(&mut samples, segments.len(), budget, open);
    Ok(samples)
}

/// LOCAL REFINEMENT of a closed chain's march (`chain/closed.rs`): one station
/// at the halfway edge parameter of every failing in-segment interval, each
/// named by its left sample. The station is the CONTINUATION of that left
/// station under the march's own branch guard (`stations::continue_station`),
/// its halfway contacts and the left station's (now over a half interval) are
/// re-solved as the march solves them, and the new stations are probed for a
/// fold exactly as the march probes its own. Grid stations — the junctions
/// with them — are never moved or re-solved. Chord parameters are NOT
/// re-assigned here (`assign_chain_parameters`).
pub(in crate::blend) fn insert_chain_stations(
    segments: &[ChainSegment<'_>],
    radius: f64,
    budget: &ChainBudget,
    samples: &mut Vec<ChainSample>,
    failing: &[usize],
) -> Result<(), ChainMarchError> {
    let mut touched: Vec<usize> = failing.iter().map(|&index| samples[index].segment).collect();
    touched.sort_unstable();
    touched.dedup();
    let lefts: Vec<(usize, isize, f64)> = failing
        .iter()
        .map(|&index| (samples[index].segment, samples[index].position, samples[index].offset))
        .collect();
    for index in touched {
        let segment = &segments[index];
        let per_segment = budget.count(index);
        let (rho1, rho2) = signed_radii(
            segment.edge,
            segment.first.face,
            segment.first.coedge,
            segment.second.face,
            segment.second.coedge,
            radius,
        )?;
        let rho = [rho1, rho2];
        let surface1 = &segment.first.face.surface;
        let surface2 = &segment.second.face.surface;
        let edge = segment.edge;
        let span = edge.t1 - edge.t0;
        let scale = march_model_scale(&edge.curve, edge.t0, edge.t1, radius)?;
        let station_step = span.abs() / per_segment as f64;
        let section_at = |t: f64| -> Result<(Vec3, Vec3), KernelRefusal> {
            let (section_point, tangent) = march_section(edge, t, station_step, "blend chain march")?;
            Ok((section_point, if segment.forward { tangent } else { tangent.scale(-1.0) }))
        };
        let solve_centered = |t: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3), KernelRefusal> {
            let (section_point, tangent) = section_at(t)?;
            solve_station_centered(surface1, surface2, rho, seed, section_point, tangent, scale)
        };
        let hop = |left: &Continued, right: &Continued| {
            branch_hop([surface1, surface2], edge, &|_| rho, left, right)
        };
        let contacts_at = |t: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3, Vec3, Vec3), KernelRefusal> {
            let (uv, _) = solve_centered(t, seed)?;
            let (section_point, tangent) = section_at(t)?;
            let (_, p1, p2, center) = tangency_residual(surface1, surface2, rho, uv, section_point, tangent)?;
            Ok((uv, p1, p2, center))
        };
        // A halfway contact continued from `from` under the branch guard, as
        // the march solves its own; a non-converging Newton is a typed station
        // failure at the halfway parameter.
        let halfway = |from: &Continued, position: isize, t: f64| -> Result<(Vec3, Vec3), ChainMarchError> {
            let node = continue_station(&solve_centered, &hop, from, t, 0).map_err(|error| {
                if is_tangency_newton(&error) {
                    ChainMarchError::Station(ChainStationFailure {
                        segment: index,
                        position,
                        t,
                        from_position: position,
                        from_t: from.t,
                        seed_uv: from.uv,
                        error,
                    })
                } else {
                    ChainMarchError::Other(error)
                }
            })?;
            let (section_point, tangent) = section_at(t)?;
            let (_, p1, p2, _) = tangency_residual(surface1, surface2, rho, node.uv, section_point, tangent)?;
            Ok((p1, p2))
        };
        for &(_, position, offset) in lefts.iter().filter(|left| left.0 == index) {
            let left_index = samples
                .iter()
                .position(|sample| sample.segment == index && sample.position == position && sample.offset == offset)
                .expect("refined interval's left station");
            // The next station along the segment: a grid station, the
            // segment's own end station (position `per_segment`), or one
            // inserted earlier in this round.
            let right_index = (0..samples.len())
                .filter(|&other| {
                    samples[other].segment == index
                        && samples[other].position <= per_segment as isize
                        && chain_order(&samples[other], &samples[left_index]) == std::cmp::Ordering::Greater
                })
                .min_by(|&a, &b| chain_order(&samples[a], &samples[b]))
                .expect("refined interval's right station");
            let left = &samples[left_index];
            let right = &samples[right_index];
            let key = 0.5 * ((left.position as f64 + left.offset) + (right.position as f64 + right.offset));
            let new_position = key.floor() as isize;
            let new_offset = key - new_position as f64;
            let (t_left, t_right) = (left.t, right.t);
            let left_uv = [left.station.uv1[0], left.station.uv1[1], left.station.uv2[0], left.station.uv2[1]];
            let retained = Continued {
                t: t_right,
                uv: [right.station.uv1[0], right.station.uv1[1], right.station.uv2[0], right.station.uv2[1]],
                center: Some(right.station.center),
            };
            let from = Continued { t: t_left, uv: left_uv, center: Some(left.station.center) };
            let t = 0.5 * (t_left + t_right);
            let node = continue_station(&solve_centered, &hop, &from, t, 0).map_err(|error| {
                if is_tangency_newton(&error) {
                    ChainMarchError::Station(ChainStationFailure {
                        segment: index,
                        position: new_position,
                        t,
                        from_position: left.position,
                        from_t: t_left,
                        seed_uv: left_uv,
                        error,
                    })
                } else {
                    ChainMarchError::Other(error)
                }
            })?;
            // The inserted station must also continue INTO its retained right
            // neighbour (a grid station, or the junction's own end station):
            // a cached right that a refined left no longer reaches is the
            // defect `MarchFrame::march_interval` re-seeds; here the right is
            // fixed, so the incoherence is reported, never waived.
            if let Some(reason) = hop(&node, &retained) {
                return Err(ChainMarchError::Incoherent(ChainStationFailure {
                    segment: index,
                    position: new_position,
                    t,
                    from_position: new_position,
                    from_t: t,
                    seed_uv: node.uv,
                    error: reason,
                }));
            }
            let (section_point, tangent) = section_at(t)?;
            let (_, p1, p2, center) = tangency_residual(surface1, surface2, rho, node.uv, section_point, tangent)?;
            let n1 = raw_normal(surface1, node.uv[0], node.uv[1])?;
            let n2 = raw_normal(surface2, node.uv[2], node.uv[3])?;
            let cos_alpha = rho1.signum() * rho2.signum() * n1.dot(n2);
            let weight = ((1.0 + cos_alpha) * 0.5).max(0.0).sqrt();
            if weight <= 1e-6 {
                return Err(KernelRefusal::unsupported(KernelStage::Refine, "tangent_faces", "blend: faces are tangent at a chain station").into());
            }
            let apex = apex_point(p1, n1, p2, n2, center)?;
            let (left_p1, left_p2) = halfway(&from, new_position, 0.5 * (t_left + t))?;
            let (mid_p1, mid_p2) = halfway(&node, new_position, 0.5 * (t + t_right))?;
            samples[left_index].midpoint = Some([left_p1, left_p2]);
            samples.push(ChainSample {
                segment: index,
                position: new_position,
                offset: new_offset,
                t,
                station: Station {
                    uv1: [node.uv[0], node.uv[1]],
                    uv2: [node.uv[2], node.uv[3]],
                    p1,
                    p2,
                    center,
                    weight,
                    apex,
                },
                parameter: 0.0,
                midpoint: Some([mid_p1, mid_p2]),
            });
        }
        // The refined segment is probed for a fold exactly as the march probes
        // its own stations: all of them in order along the segment, through its
        // end station, so the end differences and the peak refinement read
        // neighbours as the march's do — now with the inserted stations among them.
        let mut ordered: Vec<&ChainSample> = samples
            .iter()
            .filter(|sample| sample.segment == index && (0..=per_segment as isize).contains(&sample.position))
            .filter(|sample| sample.position < per_segment as isize || sample.offset == 0.0)
            .collect();
        ordered.sort_by(|a, b| chain_order(a, b));
        let probed: Vec<(f64, [f64; 4])> = ordered
            .iter()
            .map(|sample| (sample.t, [sample.station.uv1[0], sample.station.uv1[1], sample.station.uv2[0], sample.station.uv2[1]]))
            .collect();
        let probe = |t: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3, Vec3, Vec3), KernelRefusal> {
            contacts_at(t, seed)
        };
        let radius_at = |_: f64| radius.abs();
        check_wall_fold(
            &radius_at,
            span,
            &probed,
            &probe,
            segment.edge,
            [segment.first.face, segment.second.face],
            scale,
        )?;
    }
    Ok(())
}

/// Exact contacts at interior fractions of every in-segment interval — the
/// closed chain's local-path rail verdict and the `BREP_DEBUG_CHAIN_DENSE_RAILS`
/// diagnostic: each solved as the
/// continuation of the interval's left station under the branch guard, as the
/// march solves its own. A contact that does not solve is counted and its
/// interval named (`unsolved_at`), never read as a distance.
pub(in crate::blend) struct DenseContacts {
    /// (left sample, next sample, fraction of the interval, contact 1, contact 2).
    pub(in crate::blend) solved: Vec<(usize, usize, f64, Vec3, Vec3)>,
    /// The solved ball's centre for each entry of `solved`, in the same order
    /// (`None` when the continuation returned no centre).
    pub(in crate::blend) centers: Vec<Option<Vec3>>,
    pub(in crate::blend) unsolved: usize,
    /// The left sample of every interval with a contact that did not solve.
    pub(in crate::blend) unsolved_at: Vec<usize>,
}

pub(in crate::blend) fn chain_dense_contacts(
    segments: &[ChainSegment<'_>],
    radius: f64,
    budget: &ChainBudget,
    samples: &[ChainSample],
    subdivisions: usize,
) -> Result<DenseContacts, KernelRefusal> {
    let mut out = DenseContacts { solved: Vec::new(), centers: Vec::new(), unsolved: 0, unsolved_at: Vec::new() };
    for (index, segment) in segments.iter().enumerate() {
        let per_segment = budget.count(index);
        let (rho1, rho2) = signed_radii(
            segment.edge,
            segment.first.face,
            segment.first.coedge,
            segment.second.face,
            segment.second.coedge,
            radius,
        )?;
        let rho = [rho1, rho2];
        let surface1 = &segment.first.face.surface;
        let surface2 = &segment.second.face.surface;
        let edge = segment.edge;
        let span = edge.t1 - edge.t0;
        let scale = march_model_scale(&edge.curve, edge.t0, edge.t1, radius)?;
        let station_step = span.abs() / per_segment as f64;
        let section_at = |t: f64| -> Result<(Vec3, Vec3), KernelRefusal> {
            let (section_point, tangent) = march_section(edge, t, station_step, "blend chain march")?;
            Ok((section_point, if segment.forward { tangent } else { tangent.scale(-1.0) }))
        };
        let solve_centered = |t: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3), KernelRefusal> {
            let (section_point, tangent) = section_at(t)?;
            solve_station_centered(surface1, surface2, rho, seed, section_point, tangent, scale)
        };
        let hop = |left: &Continued, right: &Continued| {
            branch_hop([surface1, surface2], edge, &|_| rho, left, right)
        };
        let mut order: Vec<usize> = (0..samples.len())
            .filter(|&other| samples[other].segment == index && (0..=per_segment as isize).contains(&samples[other].position))
            .filter(|&other| samples[other].position < per_segment as isize || samples[other].offset == 0.0)
            .collect();
        order.sort_by(|&a, &b| chain_order(&samples[a], &samples[b]));
        for pair in order.windows(2) {
            let (left, next) = (&samples[pair[0]], &samples[pair[1]]);
            let from = Continued {
                t: left.t,
                uv: [left.station.uv1[0], left.station.uv1[1], left.station.uv2[0], left.station.uv2[1]],
                center: Some(left.station.center),
            };
            for step in 1..subdivisions {
                let fraction = step as f64 / subdivisions as f64;
                let t = left.t + fraction * (next.t - left.t);
                let Ok(node) = continue_station(&solve_centered, &hop, &from, t, 0) else {
                    out.unsolved += 1;
                    out.unsolved_at.push(pair[0]);
                    continue;
                };
                let (section_point, tangent) = section_at(t)?;
                let (_, p1, p2, _) = tangency_residual(surface1, surface2, rho, node.uv, section_point, tangent)?;
                out.solved.push((pair[0], pair[1], fraction, p1, p2));
                out.centers.push(node.center);
            }
        }
    }
    Ok(out)
}

/// The order stations take along a segment: grid position, then a refined
/// station's offset inside its interval.
fn chain_order(a: &ChainSample, b: &ChainSample) -> std::cmp::Ordering {
    a.segment
        .cmp(&b.segment)
        .then(a.position.cmp(&b.position))
        .then(a.offset.total_cmp(&b.offset))
}

/// Global chord parameters for every sample of a marched chain (the march's
/// own assignment, also re-run after local refinement inserts stations).
/// With no inserted station the chords are summed in the same order as the
/// march always summed them.
pub(in crate::blend) fn assign_chain_parameters(
    samples: &mut [ChainSample],
    segment_count: usize,
    budget: &ChainBudget,
    open: bool,
) {
    // Global chord parameters over the IN-SEGMENT stations (position
    // 0..CHAIN_PER_SEGMENT-1 per segment, with any inserted station in its
    // interval, in chain order), then assign overshoot samples by
    // extrapolating with their own chords.
    let mut order: Vec<usize> = (0..samples.len())
        .filter(|&index| (0..budget.count(samples[index].segment) as isize).contains(&samples[index].position))
        .collect();
    order.sort_by(|&a, &b| chain_order(&samples[a], &samples[b]));
    let mut accumulated = 0.0;
    let mut previous: Option<Vec3> = None;
    for &sample_index in &order {
        let midpoint = samples[sample_index]
            .station
            .p1
            .add(samples[sample_index].station.p2)
            .scale(0.5);
        if let Some(previous_point) = previous {
            accumulated += midpoint.sub(previous_point).length();
        }
        samples[sample_index].parameter = accumulated;
        previous = Some(midpoint);
    }
    // Wrap chord back to the chain start (CLOSED chains only; an OPEN chain
    // parameterises its in-segment stations onto [0, 1] with no wrap term so
    // the free ends sit at the parameter extremes, clamped like fit_open_rows).
    if !open {
        let first_midpoint = {
            let sample = samples
                .iter()
                .find(|sample| sample.segment == 0 && sample.position == 0 && sample.offset == 0.0)
                .expect("chain start sample");
            sample.station.p1.add(sample.station.p2).scale(0.5)
        };
        accumulated += first_midpoint.sub(previous.unwrap()).length();
    }
    let total = accumulated.max(1e-12);
    for sample in samples.iter_mut() {
        sample.parameter /= total;
    }
    // Overshoot samples: REAL chord distances from the boundary
    // in-segment stations (linear extrapolation misparameterises them
    // when the neighbouring segment's station spacing differs, and the
    // support pieces' crossing region lies exactly there).
    for segment in 0..segment_count {
        let per_segment = budget.count(segment);
        let sample_at = |samples: &[ChainSample], position: isize| -> usize {
            samples
                .iter()
                .position(|sample| sample.segment == segment && sample.position == position && sample.offset == 0.0)
                .expect("chain sample present")
        };
        let midpoint = |samples: &[ChainSample], index: usize| -> Vec3 {
            samples[index]
                .station
                .p1
                .add(samples[index].station.p2)
                .scale(0.5)
        };
        // Below position 0.
        let anchor = sample_at(samples, 0);
        let mut accumulated = samples[anchor].parameter;
        let mut previous_point = midpoint(samples, anchor);
        for position in (-(CHAIN_OVERSHOOT as isize)..0).rev() {
            let index = sample_at(samples, position);
            let point = midpoint(samples, index);
            accumulated -= point.sub(previous_point).length() / total;
            samples[index].parameter = accumulated;
            previous_point = point;
        }
        // Above the last in-segment station: the grid's last position, or a
        // station refinement inserted after it.
        let anchor = *order
            .iter()
            .filter(|&&index| samples[index].segment == segment)
            .last()
            .expect("chain sample present");
        let mut accumulated = samples[anchor].parameter;
        let mut previous_point = midpoint(samples, anchor);
        for position in per_segment as isize..=(per_segment + CHAIN_OVERSHOOT) as isize
        {
            let index = sample_at(samples, position);
            let point = midpoint(samples, index);
            accumulated += point.sub(previous_point).length() / total;
            samples[index].parameter = accumulated;
            previous_point = point;
        }
    }
    station_trace(samples, budget);
    // Re-origin the global parameter at segment 0's MIDDLE station so the
    // blend seam falls far from every junction (a seam ON a junction
    // collides with that junction's spoke crossing).  Kept UNWRAPPED so
    // every segment's sample window stays contiguous (segment 0 spans
    // negative params); the global closed fit wraps with rem_euclid.  An
    // OPEN chain has no seam, so its parameter stays anchored at the start
    // free end (segment 0's position 0 == 0).
    if !open {
        let p_mid = samples
            .iter()
            .find(|sample| sample.segment == 0 && sample.position == budget.count(0) as isize / 2 && sample.offset == 0.0)
            .map(|sample| sample.parameter)
            .expect("chain mid sample");
        for sample in samples.iter_mut() {
            sample.parameter -= p_mid;
        }
    }
}

/// `BREP_BLEND_STATION_TRACE=1` prints every marched chain station once the
/// global chord parameters are assigned: the two contacts, the ball centre,
/// both parameter feet on the mates and the parameter the row fit will use.
///
/// The fitted rows are what a built wall carries, and reading them back off
/// the face cannot tell a station apart from the interpolant through it — a
/// branch hop between two stations hides inside one cubic span. This prints
/// the samples themselves, so the fit and the march can be told apart.
fn station_trace(samples: &[ChainSample], budget: &ChainBudget) {
    if std::env::var("BREP_BLEND_STATION_TRACE").ok().as_deref() != Some("1") {
        return;
    }
    eprintln!(
        "blend chain stations: {} sample(s) ({})",
        samples.len(),
        budget.describe()
    );
    let mut ordered: Vec<&ChainSample> = samples.iter().collect();
    ordered.sort_by(|a, b| chain_order(a, b));
    let mut previous: Option<Vec3> = None;
    for sample in ordered {
        let station = &sample.station;
        let step = previous
            .map(|point| station.center.sub(point).length())
            .unwrap_or(f64::NAN);
        eprintln!(
            "  seg {} pos {:>3} t {:.9} p1 ({:.9}, {:.9}, {:.9}) p2 ({:.9}, {:.9}, {:.9}) \
             c ({:.9}, {:.9}, {:.9}) uv1 ({:.9}, {:.9}) uv2 ({:.9}, {:.9}) w {:.9} dc {:.9}",
            sample.segment,
            sample.position,
            sample.parameter,
            station.p1.x,
            station.p1.y,
            station.p1.z,
            station.p2.x,
            station.p2.y,
            station.p2.z,
            station.center.x,
            station.center.y,
            station.center.z,
            station.uv1[0],
            station.uv1[1],
            station.uv2[0],
            station.uv2[1],
            station.weight,
            step,
        );
        previous = Some(station.center);
    }
}