Skip to main content

brep_kernel/blending/blend/
mod.rs

1//! General rolling-ball blends (Golovanov §4.9 + §6.9 surgery).
2//!
3//! The blend surface is the rational surface (4.9.1) built from three
4//! curves: the two support curves cr(t) ⊂ F1 and cs(t) ⊂ F2 where a
5//! sphere of radius ρ touches both faces, and the middle curve at the
6//! intersection of the two tangent planes, weighted w(t) = cos(α/2)
7//! (4.9.3).  The tangency system (4.9.2) + the section-plane constraint
8//! (4.9.5) — the sphere center pinned to the normal plane of the edge —
9//! is solved per station by Newton with a finite-difference Jacobian.
10//!
11//! Topology is rebuilt the §6.9 way, with NO tool solid and NO boolean:
12//! each mating face's loop swaps the blended edge's coedge for a coedge
13//! on its support curve (trimming the adjacent seam edge when the face
14//! is a closed carrier with seam-in-one-loop topology), and the blend
15//! face is inserted with its own seam.  Open edges take the same route
16//! (`edge/open.rs`), and a whole SELECTION goes through `network.rs`, which
17//! marches every stripe against the original solid and solves each shared
18//! corner from its ball before anything is cut.
19
20mod carve;
21mod chain;
22mod collapse;
23mod corner;
24mod edge;
25mod fold;
26mod miter;
27mod network;
28mod planar_chart;
29mod restrict;
30pub(super) mod rollover;
31mod rollover_surgery;
32pub(crate) mod runout;
33mod stations;
34mod rows;
35mod track_fit;
36
37thread_local! {
38    static REFINE_OFF_FOR_TEST: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
39}
40
41/// Whether the open march's local station refinement runs. Escape hatch
42/// `BREP_BLEND_REFINE=0` restores the uniform ladder exactly; a test turns it
43/// off for its own thread with `set_refinement_off_for_test`.
44pub(crate) fn station_refinement_on() -> bool {
45    std::env::var("BREP_BLEND_REFINE").as_deref() != Ok("0")
46        && !REFINE_OFF_FOR_TEST.with(|off| off.get())
47}
48
49thread_local! {
50    static BLEND_NOTES: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
51}
52
53thread_local! {
54    /// Set only while the open-pad composition (`fillet/edges.rs`, the
55    /// 2026-10-17 face-ring report) builds its two stages on this thread.
56    static OPEN_PAD_NETWORK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
57}
58
59/// Runs `build` as the open-pad composition's network: the open march takes
60/// a station where a contact crosses its carrier's domain boundary and the
61/// row fit breaks there (`march_open_stations_core`), and the march extends
62/// past a section end that lies outside the overshoot
63/// (`open_station_parameters`). Both are what the composition's walls need
64/// (the F6 blend rail of the reported ring stood 7.1e-5 off its carrier at
65/// the boundary; the pads' stripes start before their trims) and both move
66/// a body the plain network already built (2026-10-18: the reported model's
67/// F6 moved by a refinement rung and a broken row), so outside the
68/// composition every network is built exactly as before. Restores the flag
69/// after.
70pub(crate) fn with_open_pad_network<T>(build: impl FnOnce() -> T) -> T {
71    /// Restores the previous flag on every exit path, an unwinding panic
72    /// included, so a later network on this thread never inherits it.
73    struct Reset(bool);
74    impl Drop for Reset {
75        fn drop(&mut self) {
76            OPEN_PAD_NETWORK.with(|flag| flag.set(self.0));
77        }
78    }
79    let _reset = Reset(OPEN_PAD_NETWORK.with(|flag| flag.replace(true)));
80    build()
81}
82
83/// Whether the march runs as the open-pad composition's network (see
84/// [`with_open_pad_network`]).
85pub(in crate::blend) fn open_pad_network_on() -> bool {
86    OPEN_PAD_NETWORK.with(|flag| flag.get())
87}
88
89
90thread_local! {
91    static EXACT_LANE_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
92    static EXACT_REVOLUTION_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
93    static EXACT_RIM_LANE_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
94    static ACCEPT_ONCE_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
95    static CHAIN_ARC_STATIONS_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
96    static TAPER_NETWORK_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
97    static ROLLOVER_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
98    static EXACT_TRIM_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
99    static UNEQUAL_MITER_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
100    static NO_COMMON_BALL_STAR_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
101    static CROSSED_RAILS_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
102}
103
104/// Off, the network names the stripe `crossed_rails` and the selection
105/// falls through to the cutter composition, whose body is the union of the
106/// canal removals (a ridge). `BREP_BLEND_CROSSED_RAILS=0` is the escape
107/// hatch, `=1` forces it on; `set_crossed_rails_for_test` forces either way
108/// on a test's thread.
109const CROSSED_RAILS_DEFAULT: bool = true;
110
111pub(crate) fn crossed_rails_on() -> bool {
112    if let Some(forced) = CROSSED_RAILS_FOR_TEST.with(|forced| forced.get()) {
113        return forced;
114    }
115    match std::env::var("BREP_BLEND_CROSSED_RAILS").as_deref() {
116        Ok("0") => false,
117        Ok("1") => true,
118        _ => CROSSED_RAILS_DEFAULT,
119    }
120}
121
122/// Off, the network refuses `unequal_corner_radius` or `n_sided_corner` by
123/// name as before. `BREP_BLEND_RIBBON_STAR=0` is the escape hatch, `=1`
124/// forces it on; `set_no_common_ball_star_for_test` forces it on a test's
125/// thread.
126const NO_COMMON_BALL_STAR_DEFAULT: bool = true;
127
128pub(crate) fn no_common_ball_star_on() -> bool {
129    if let Some(forced) = NO_COMMON_BALL_STAR_FOR_TEST.with(|forced| forced.get()) {
130        return forced;
131    }
132    match std::env::var("BREP_BLEND_RIBBON_STAR").as_deref() {
133        Ok("0") => false,
134        Ok("1") => true,
135        _ => NO_COMMON_BALL_STAR_DEFAULT,
136    }
137}
138
139/// The corner has no common ball; the point both shared-face rails stop at is
140/// their rail/rail point (`solve_rail_rail_point`), the seam between the blends
141/// is marched on the larger stripe at the smaller stripe's own canal level, and
142/// — the seam leaving the smaller strip through its side rail — the larger
143/// stripe runs onto that side face and is closed by its section there (the
144/// asymmetric miter's connector), so the side face gets two rail vertices.
145/// `BREP_BLEND_UNEQUAL_MITER=0` is the escape hatch, `=1` forces it on;
146/// `set_unequal_miter_for_test` forces it on a test's thread.
147const UNEQUAL_MITER_DEFAULT: bool = true;
148
149pub(crate) fn unequal_miter_on() -> bool {
150    if let Some(forced) = UNEQUAL_MITER_FOR_TEST.with(|forced| forced.get()) {
151        return forced;
152    }
153    match std::env::var("BREP_BLEND_UNEQUAL_MITER").as_deref() {
154        Ok("0") => false,
155        Ok("1") => true,
156        _ => UNEQUAL_MITER_DEFAULT,
157    }
158}
159
160/// Off, the closed lane names the rollover and declines as it did when the
161/// detection landed (`rollover.rs`), and the ladder falls through to the
162/// cutter, whose body carries a crease. `BREP_BLEND_ROLLOVER=0` is the
163/// escape hatch, `=1` forces it on; `set_rollover_for_test` forces either
164/// way on a test's own thread.
165const ROLLOVER_DEFAULT: bool = true;
166
167pub(crate) fn rollover_on() -> bool {
168    if let Some(forced) = ROLLOVER_FOR_TEST.with(|forced| forced.get()) {
169        return forced;
170    }
171    match std::env::var("BREP_BLEND_ROLLOVER").as_deref() {
172        Ok("0") => false,
173        Ok("1") => true,
174        _ => ROLLOVER_DEFAULT,
175    }
176}
177
178/// OPEN stripes: a straight edge between planar mates is one section
179/// extruded, so the network marches its stations once and skips the ladder's
180/// refinement, fold probe, dense centres and ball-resolve wall reading (the
181/// wall is read off the exact rows instead); the body is the ladder's, byte
182/// for byte. Landed behind this switch, off by default, until the census
183/// promotes it: `BREP_BLEND_EXACT_LANE=1` turns it on, `=0` off; a test
184/// forces either way on its own thread with `set_exact_lane_for_test`.
185const EXACT_LANE_DEFAULT: bool = true;
186
187/// The exact torus is not the fitted rows' body. `BREP_BLEND_EXACT_RIM=1` turns
188/// it on, `=0` off; `set_exact_rim_lane_for_test` forces it on a test's own
189/// thread.
190const EXACT_RIM_LANE_DEFAULT: bool = true;
191
192pub(crate) fn exact_rim_lane_on() -> bool {
193    if let Some(forced) = EXACT_RIM_LANE_FOR_TEST.with(|forced| forced.get()) {
194        return forced;
195    }
196    match std::env::var("BREP_BLEND_EXACT_RIM").as_deref() {
197        Ok("0") => false,
198        Ok("1") => true,
199        _ => EXACT_RIM_LANE_DEFAULT,
200    }
201}
202
203pub(crate) fn exact_lane_on() -> bool {
204    if let Some(forced) = EXACT_LANE_FOR_TEST.with(|forced| forced.get()) {
205        return forced;
206    }
207    match std::env::var("BREP_BLEND_EXACT_LANE").as_deref() {
208        Ok("0") => false,
209        Ok("1") => true,
210        _ => EXACT_LANE_DEFAULT,
211    }
212}
213
214/// The exact rows are not the fitted rows' bytes, so the lane landed behind
215/// this switch OFF and was promoted ON by its census (the whole kernel
216/// battery, the 96-case manifest at its baseline and the render HLR pins
217/// with the switch on). `BREP_BLEND_EXACT_REVOLUTION=0` is the hatch back to
218/// the fitted ladder, `=1` forces the lane; a test forces either way on its
219/// own thread with `set_exact_revolution_for_test`. The lane declines by
220/// name to the ladder (`count.march.exact_revolution_declined`) whenever its
221/// witness does not hold.
222const EXACT_REVOLUTION_DEFAULT: bool = true;
223
224pub(crate) fn exact_revolution_on() -> bool {
225    if let Some(forced) = EXACT_REVOLUTION_FOR_TEST.with(|forced| forced.get()) {
226        return forced;
227    }
228    match std::env::var("BREP_BLEND_EXACT_REVOLUTION").as_deref() {
229        Ok("0") => false,
230        Ok("1") => true,
231        _ => EXACT_REVOLUTION_DEFAULT,
232    }
233}
234
235/// The surgery lane's exact support trim (slice 1b of the box fillet's
236/// hotspots): on an EXACT EXTRUSION row whose mate is a plane, the rail is
237/// a straight line and the plane's (u, v) is affine, so the rail's support
238/// trim is the straight line in uv between the feet of the rail's two ends,
239/// read once as shipped at the pcurve floor (`exact_planar_trim`); the
240/// march trim's read, the track refit and its three reads are skipped. The
241/// line is not the refit's curve (two control points against its fitted
242/// knots), so the body's pcurve bits move on every box-like fixture; the
243/// lane shipped behind this switch, off, until its census (the surgery
244/// record of 2026-10-11: battery, suites and case gate unchanged, one HLR
245/// input digest moving in pcurve bytes with its drawing byte-identical)
246/// re-pinned what moves, and it is ON by default since 2026-10-12.
247/// `BREP_BLEND_EXACT_TRIM=0` is the escape hatch back to the refit road
248/// (`=1` the lane, as before); `set_exact_trim_for_test` forces either way
249/// on a test's own thread.
250const EXACT_TRIM_DEFAULT: bool = true;
251
252pub(crate) fn exact_trim_on() -> bool {
253    if let Some(forced) = EXACT_TRIM_FOR_TEST.with(|forced| forced.get()) {
254        return forced;
255    }
256    match std::env::var("BREP_BLEND_EXACT_TRIM").as_deref() {
257        Ok("0") => false,
258        Ok("1") => true,
259        _ => EXACT_TRIM_DEFAULT,
260    }
261}
262
263/// Same landing rule and switch shape as [`exact_lane_on`]:
264/// `BREP_BLEND_ACCEPT_ONCE=1` / `=0`, default below.
265const ACCEPT_ONCE_DEFAULT: bool = true;
266
267pub(crate) fn accept_once_on() -> bool {
268    if let Some(forced) = ACCEPT_ONCE_FOR_TEST.with(|forced| forced.get()) {
269        return forced;
270    }
271    match std::env::var("BREP_BLEND_ACCEPT_ONCE").as_deref() {
272        Ok("0") => false,
273        Ok("1") => true,
274        _ => ACCEPT_ONCE_DEFAULT,
275    }
276}
277
278/// Equal-length chains are unchanged either way; unequal ones move their
279/// stations, so it lands behind this switch until its census:
280/// `BREP_BLEND_CHAIN_ARC_STATIONS=1` turns it on, `=0` off;
281/// `set_chain_arc_stations_for_test` forces it on a test's own thread.
282const CHAIN_ARC_STATIONS_DEFAULT: bool = false;
283
284pub(crate) fn chain_arc_stations_on() -> bool {
285    if let Some(forced) = CHAIN_ARC_STATIONS_FOR_TEST.with(|forced| forced.get()) {
286        return forced;
287    }
288    match std::env::var("BREP_BLEND_CHAIN_ARC_STATIONS").as_deref() {
289        Ok("0") => false,
290        Ok("1") => true,
291        _ => CHAIN_ARC_STATIONS_DEFAULT,
292    }
293}
294
295/// `BREP_BLEND_TAPER_NETWORK=0` is the escape hatch, `=1` forces it on;
296/// `set_taper_network_for_test` forces it on a test's own thread.
297const TAPER_NETWORK_DEFAULT: bool = true;
298
299pub(crate) fn taper_network_on() -> bool {
300    if let Some(forced) = TAPER_NETWORK_FOR_TEST.with(|forced| forced.get()) {
301        return forced;
302    }
303    match std::env::var("BREP_BLEND_TAPER_NETWORK").as_deref() {
304        Ok("0") => false,
305        Ok("1") => true,
306        _ => TAPER_NETWORK_DEFAULT,
307    }
308}
309
310#[derive(Clone, Debug, PartialEq)]
311pub(crate) enum StripeRadius {
312    Constant(f64),
313    /// `(edge fraction, radius)` stops sorted by fraction, interpolated
314    /// linearly between and held flat past the ends: `blend_edge_variable`'s
315    /// convention, which the law entries sample into.
316    Stops(Vec<(f64, f64)>),
317}
318
319impl StripeRadius {
320    /// From a stop list: one constant when every stop carries the same
321    /// radius (to 1e-12 relative, the single-edge lane's own test).
322    pub(crate) fn from_stops(stops: &[(f64, f64)]) -> Self {
323        let mut sorted = stops.to_vec();
324        sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
325        match sorted.first() {
326            Some(&(_, first)) if sorted.iter().all(|(_, r)| (r - first).abs() <= 1e-12 * (1.0 + first.abs())) => {
327                StripeRadius::Constant(first)
328            }
329            _ => StripeRadius::Stops(sorted),
330        }
331    }
332
333    /// The radius at an edge fraction in `[0, 1]` (clamped).
334    pub(crate) fn at_fraction(&self, fraction: f64) -> f64 {
335        match self {
336            StripeRadius::Constant(radius) => *radius,
337            StripeRadius::Stops(stops) => {
338                let fraction = fraction.clamp(0.0, 1.0);
339                if fraction <= stops[0].0 {
340                    return stops[0].1;
341                }
342                for pair in stops.windows(2) {
343                    if fraction <= pair[1].0 {
344                        let width = (pair[1].0 - pair[0].0).max(1e-12);
345                        return pair[0].1 + (pair[1].1 - pair[0].1) * (fraction - pair[0].0) / width;
346                    }
347                }
348                stops[stops.len() - 1].1
349            }
350        }
351    }
352
353    /// The constant, for a constant law.
354    pub(crate) fn constant(&self) -> Option<f64> {
355        match self {
356            StripeRadius::Constant(radius) => Some(*radius),
357            StripeRadius::Stops(_) => None,
358        }
359    }
360
361    /// The largest radius the law takes.
362    pub(crate) fn largest(&self) -> f64 {
363        match self {
364            StripeRadius::Constant(radius) => *radius,
365            StripeRadius::Stops(stops) => stops.iter().map(|(_, r)| *r).fold(0.0, f64::max),
366        }
367    }
368
369    /// The law's END value at the edge's start (`at_start`) or finish: what
370    /// the user asked for at that vertex, and the constant every corner solve
371    /// there uses (C2).
372    pub(crate) fn end(&self, at_start: bool) -> f64 {
373        self.at_fraction(if at_start { 0.0 } else { 1.0 })
374    }
375
376    /// Whether every radius is positive and finite.
377    pub(crate) fn is_positive(&self) -> bool {
378        match self {
379            StripeRadius::Constant(radius) => radius.is_finite() && *radius > 0.0,
380            StripeRadius::Stops(stops) => !stops.is_empty() && stops.iter().all(|(_, r)| r.is_finite() && *r > 0.0),
381        }
382    }
383}
384
385
386
387
388
389
390
391
392/// Record what a blend that SHIPS needs its feature to say about it — today,
393/// a march whose rails still stand off their carriers after its station
394/// ladder and local refinement ran out. Drained per feature by the history
395/// loop ([`take_blend_notes`]), beside the soundness acceptance's repairs.
396pub(crate) fn record_blend_note(note: String) {
397    BLEND_NOTES.with(|notes| notes.borrow_mut().push(note));
398}
399
400/// Take every blend note recorded on this thread since the last call.
401/// Blending runs on the feature's own thread (no parallel march), so the
402/// history loop's drain sees every note its feature recorded.
403pub fn take_blend_notes() -> Vec<String> {
404    BLEND_NOTES.with(|notes| std::mem::take(&mut *notes.borrow_mut()))
405}
406
407/// The ledger's length, and a way back to it: a lane that refuses takes back
408/// the notes it recorded, so a note never describes a wall that did not ship.
409pub(crate) fn blend_notes_mark() -> usize {
410    BLEND_NOTES.with(|notes| notes.borrow().len())
411}
412
413pub(crate) fn blend_notes_truncate(mark: usize) {
414    BLEND_NOTES.with(|notes| notes.borrow_mut().truncate(mark));
415}
416
417/// A fillet chain wall that SHIPPED short of the construction request its
418/// accepted rung made (`KernelTolerances::model` on the wall between its
419/// rails): which wall — the blend face's name and its exact surface — what
420/// was asked, what the wall reads, and the typed budget that ran out.
421#[derive(Clone, Debug)]
422pub(crate) struct WallModelReport {
423    pub(crate) name: Option<String>,
424    pub(crate) surface: crate::NurbsSurface,
425    pub(crate) request: f64,
426    pub(crate) residual: f64,
427    /// What else the lane measured that the message must say (an unaccepted
428    /// rung's deficit, a ladder rung's local refinement), verbatim.
429    pub(crate) detail: Option<String>,
430    pub(crate) budget: crate::ApproximationBudget,
431}
432
433thread_local! {
434    static BLEND_APPROXIMATIONS: std::cell::RefCell<Vec<WallModelReport>> = const { std::cell::RefCell::new(Vec::new()) };
435}
436
437thread_local! {
438    static BLEND_OPERATION_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
439}
440
441/// One blend OPERATION's scope on this thread: every public blend entry point
442/// (and the history loop, around each feature) holds one. The OUTERMOST
443/// entry drops whatever an earlier operation recorded and nobody consumed —
444/// a direct API caller consumes ([`take_blend_approximations`]) after the
445/// operation whose reports it wants, before starting the next — while a
446/// nested entry (a fillet's per-edge chain blend) clears nothing, so every
447/// report of the operation in flight is kept, however many there are.
448pub(crate) struct BlendOperation;
449
450impl BlendOperation {
451    pub(crate) fn enter() -> BlendOperation {
452        BLEND_OPERATION_DEPTH.with(|depth| {
453            if depth.get() == 0 {
454                BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().clear());
455                timing::begin_operation();
456            }
457            depth.set(depth.get() + 1);
458        });
459        BlendOperation
460    }
461}
462
463impl Drop for BlendOperation {
464    fn drop(&mut self) {
465        BLEND_OPERATION_DEPTH.with(|depth| {
466            depth.set(depth.get().saturating_sub(1));
467            if depth.get() == 0 {
468                timing::end_operation();
469            }
470        });
471    }
472}
473
474/// Phase timing of the blend lanes, on the same switch as the boolean's
475/// `boolean.<stage>_ms` lines: set `BREP_PROFILE` and the outermost blend
476/// operation prints one `blend.<measure>=<ms>` line per measure to stderr when
477/// it ends. Off (one `Cell` read per hook), nothing is recorded.
478///
479/// Measures ACCUMULATE: a stage that runs per stripe, per rung or per
480/// component adds to one entry and bumps its count, so the ledger reads as a
481/// split of the operation, not as the worst instance. `timing.*` entries are
482/// milliseconds, `count.*` entries are plain counters.
483pub(crate) mod timing {
484    use std::cell::{Cell, RefCell};
485    use web_time::Instant;
486
487    /// One ledger entry: the measure's name, its accumulated milliseconds
488    /// (zero for a `count.*` entry) and how many times it was recorded.
489    #[derive(Clone, Debug, PartialEq)]
490    pub struct BlendTiming {
491        pub measure: String,
492        pub ms: f64,
493        pub count: u64,
494    }
495
496    thread_local! {
497        static CAPTURE: Cell<bool> = const { Cell::new(false) };
498        static ENABLED: Cell<bool> = const { Cell::new(false) };
499        static TO_STDERR: Cell<bool> = const { Cell::new(false) };
500        static LEDGER: RefCell<Vec<BlendTiming>> = const { RefCell::new(Vec::new()) };
501    }
502
503    /// Turn the ledger on (or off) for the blend operations that FOLLOW on
504    /// this thread; `BREP_PROFILE` turns it on for every operation.
505    pub fn blend_timing_capture(on: bool) {
506        CAPTURE.with(|capture| capture.set(on));
507    }
508
509    /// Take every entry recorded on this thread since the last call (or the
510    /// last outermost operation's start), sorted by measure name.
511    pub fn take_blend_timings() -> Vec<BlendTiming> {
512        let mut entries = LEDGER.with(|ledger| std::mem::take(&mut *ledger.borrow_mut()));
513        entries.sort_by(|a, b| a.measure.cmp(&b.measure));
514        entries
515    }
516
517    /// Whether the hooks record anything right now.
518    pub(crate) fn enabled() -> bool {
519        ENABLED.with(|enabled| enabled.get())
520    }
521
522    pub(super) fn begin_operation() {
523        let profile = std::env::var_os("BREP_PROFILE").is_some();
524        TO_STDERR.with(|to_stderr| to_stderr.set(profile));
525        let on = profile || CAPTURE.with(|capture| capture.get());
526        ENABLED.with(|enabled| enabled.set(on));
527        if on {
528            LEDGER.with(|ledger| ledger.borrow_mut().clear());
529        }
530    }
531
532    pub(super) fn end_operation() {
533        if TO_STDERR.with(|to_stderr| to_stderr.get()) {
534            let mut entries = LEDGER.with(|ledger| ledger.borrow().clone());
535            entries.sort_by(|a, b| a.measure.cmp(&b.measure));
536            for entry in entries {
537                if entry.measure.starts_with("timing.") {
538                    eprintln!("blend.{}={:.3} n={}", entry.measure, entry.ms, entry.count);
539                } else {
540                    eprintln!("blend.{}={}", entry.measure, entry.count);
541                }
542            }
543        }
544        ENABLED.with(|enabled| enabled.set(false));
545    }
546
547    fn add(measure: &str, ms: f64, count: u64) {
548        LEDGER.with(|ledger| {
549            let mut ledger = ledger.borrow_mut();
550            match ledger.iter_mut().find(|entry| entry.measure == measure) {
551                Some(entry) => {
552                    entry.ms += ms;
553                    entry.count += count;
554                }
555                None => ledger.push(BlendTiming { measure: measure.to_string(), ms, count }),
556            }
557        });
558    }
559
560    /// Add `ms` to a `timing.*` measure (one more occurrence).
561    pub(crate) fn record_ms(measure: &str, ms: f64) {
562        if enabled() {
563            add(measure, ms, 1);
564        }
565    }
566
567    /// Add `n` to a `count.*` measure.
568    pub(crate) fn record_count(measure: &str, n: u64) {
569        if enabled() {
570            add(measure, 0.0, n);
571        }
572    }
573
574    /// A stage timer: records its measure when stopped or dropped. Costs one
575    /// flag read when the ledger is off.
576    pub(crate) struct BlendTimer {
577        measure: &'static str,
578        started: Option<Instant>,
579    }
580
581    impl BlendTimer {
582        pub(crate) fn start(measure: &'static str) -> BlendTimer {
583            BlendTimer { measure, started: enabled().then(Instant::now) }
584        }
585
586        /// Milliseconds since the start (zero when the ledger is off).
587        pub(crate) fn elapsed_ms(&self) -> f64 {
588            self.started.map_or(0.0, |started| started.elapsed().as_secs_f64() * 1_000.0)
589        }
590
591        /// Record now and return the reading.
592        pub(crate) fn stop(mut self) -> f64 {
593            let ms = self.elapsed_ms();
594            if self.started.take().is_some() {
595                add(self.measure, ms, 1);
596            }
597            ms
598        }
599    }
600
601    impl Drop for BlendTimer {
602        fn drop(&mut self) {
603            if let Some(started) = self.started.take() {
604                add(self.measure, started.elapsed().as_secs_f64() * 1_000.0, 1);
605            }
606        }
607    }
608}
609pub use timing::{blend_timing_capture, take_blend_timings, BlendTiming};
610pub(crate) use timing::{record_count, BlendTimer};
611
612/// Record a wall that ships with its request unmet. Written by the rung that
613/// keeps its body; whether the body reaches a caller is decided at
614/// consumption ([`take_blend_approximations`]), not here. Every report of
615/// the operation in flight is kept (see [`BlendOperation`]).
616pub(crate) fn record_wall_model_report(report: WallModelReport) {
617    BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().push(report));
618}
619
620
621/// Whether any wall-model report is waiting (the history loop reads the
622/// returned bodies only then).
623pub(crate) fn blend_approximations_pending() -> bool {
624    BLEND_APPROXIMATIONS.with(|reports| !reports.borrow().is_empty())
625}
626
627/// Bit-identical surfaces: the same degrees, knots and homogeneous control
628/// points, compared by their bits.
629pub(crate) fn same_surface(a: &crate::NurbsSurface, b: &crate::NurbsSurface) -> bool {
630    let bits = |values: &[f64]| values.iter().map(|value| value.to_bits()).collect::<Vec<_>>();
631    a.degree_u == b.degree_u
632        && a.degree_v == b.degree_v
633        && bits(&a.knots_u) == bits(&b.knots_u)
634        && bits(&a.knots_v) == bits(&b.knots_v)
635        && a.control_points.len() == b.control_points.len()
636        && a.control_points.iter().zip(&b.control_points).all(|(row_a, row_b)| {
637            row_a.len() == row_b.len()
638                && row_a.iter().zip(row_b).all(|(p, q)| {
639                    [p.x, p.y, p.z, p.w].map(f64::to_bits) == [q.x, q.y, q.z, q.w].map(f64::to_bits)
640                })
641        })
642}
643
644/// Whether `face_name` names the report's wall: its exact name, or that
645/// name with the `[k]` (decimal) suffix `ensure_unique_face_names` gives a
646/// colliding name at registration. Nothing else: no prefix, no other suffix.
647fn names_the_wall(face_name: &Option<String>, report_name: &Option<String>) -> bool {
648    match (face_name, report_name) {
649        (Some(face), Some(report)) => {
650            face == report
651                || face.strip_prefix(report.as_str()).and_then(|rest| rest.strip_prefix('[')).and_then(|rest| rest.strip_suffix(']')).is_some_and(
652                    |index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()),
653                )
654        }
655        (None, None) => true,
656        _ => false,
657    }
658}
659
660/// Consume EVERY wall-model report recorded on this thread since the last
661/// call, in ONE snapshot, against ALL the bodies a call returned
662/// (`(body name, solid)`): a report becomes a typed `blend.wall_model`
663/// [`crate::Approximation`] on each body carrying its wall — a face with a
664/// bit-identical surface AND the report's name, exactly or as registration
665/// renamed it (`NAME[k]`) — and is dropped when no returned body carries it
666/// (a lane that refused, a body a later step discarded). A wall carried by
667/// two returned bodies (one surface, bit for bit, in both) is reported on
668/// both: the reading is a property of that surface. Reports for one wall
669/// collapse to the last. Never a match by name alone.
670/// `take_blend_approximations(&[])` discards everything recorded (the
671/// history loop's stale drain before a feature runs).
672pub fn take_blend_approximations(bodies: &[(&str, &crate::BrepSolid)]) -> Vec<crate::Approximation> {
673    let snapshot = BLEND_APPROXIMATIONS.with(|reports| std::mem::take(&mut *reports.borrow_mut()));
674    let mut unique: Vec<WallModelReport> = Vec::new();
675    for report in snapshot {
676        unique.retain(|kept| !(kept.name == report.name && same_surface(&kept.surface, &report.surface)));
677        unique.push(report);
678    }
679    let mut approximations = Vec::new();
680    for (body, solid) in bodies {
681        for report in &unique {
682            let carried = solid.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
683                names_the_wall(&face.name, &report.name) && same_surface(&face.surface, &report.surface)
684            });
685            if carried {
686                let wall = report.name.as_deref().unwrap_or("(unnamed)");
687                let unaccepted = matches!(report.budget.reason, crate::BudgetReason::Unaccepted | crate::BudgetReason::Unread);
688                let budget = match report.budget.mechanism {
689                    crate::BudgetMechanism::LocalRounds => format!(
690                        "local refinement stopped: {:?} after {} of {} rounds, {} of {} stations per segment",
691                        report.budget.reason, report.budget.rounds_used, report.budget.rounds_limit,
692                        report.budget.stations, report.budget.station_limit,
693                    ),
694                    crate::BudgetMechanism::StationRungs => format!(
695                        "the open march's station ladder stopped: {:?} after {} of {} global station rungs, {} of {} stations",
696                        report.budget.reason, report.budget.rounds_used, report.budget.rounds_limit,
697                        report.budget.stations, report.budget.station_limit,
698                    ),
699                };
700                let unknown = report.budget.unread > 0;
701                let rails = report.budget.measured_component == crate::MeasuredComponent::Rails;
702                let mut message = if !unaccepted && (rails || unknown) {
703                    // A request that covers the rails and the wall says WHICH
704                    // it measured, and never offers a partial reading as the
705                    // worst: with unread intervals the worst is unknown.
706                    let what = if rails {
707                        format!("the fillet wall {wall}'s support rails stand")
708                    } else {
709                        format!("the fillet wall {wall} stands")
710                    };
711                    let against = if rails {
712                        "off their exact construction (their carriers / the exact ball contacts)"
713                    } else {
714                        "from the rolling ball between its rails"
715                    };
716                    let reading = if unknown {
717                        format!(
718                            "an UNKNOWN distance {against}: {} interval(s) could not be read (the largest READABLE reading is {:.3e} mm, not the worst)",
719                            report.budget.unread, report.residual
720                        )
721                    } else {
722                        format!("{:.3e} mm {against}", report.residual)
723                    };
724                    format!(
725                        "{what} {reading}, against a construction request of {:.3e} mm (accepted at the intersection-fit bar); {budget}",
726                        report.request,
727                    )
728                } else if unaccepted {
729                    format!(
730                        "the blend wall {wall} ships from a rung that never met acceptance: its worst reading \
731                         {:.3e} mm against its acceptance bar {:.3e} mm; {budget}",
732                        report.residual, report.request,
733                    )
734                } else {
735                    format!(
736                        "the fillet wall {wall} stands {:.3e} mm from the rolling ball between its rails, against a \
737                         construction request of {:.3e} mm (accepted at the intersection-fit bar); {budget}",
738                        report.residual, report.request,
739                    )
740                };
741                if let Some(detail) = &report.detail {
742                    message = format!("{message} ({detail})");
743                }
744                approximations.push(crate::Approximation {
745                    code: if unaccepted { "blend.wall_acceptance" } else { "blend.wall_model" }.to_string(),
746                    body: body.to_string(),
747                    measured: report.residual,
748                    bar: report.request,
749                    volume_bound: None,
750                    edges: Vec::new(),
751                    budget: Some(report.budget.clone()),
752                    message,
753                });
754            }
755        }
756    }
757    approximations
758}
759
760
761pub use chain::{blend_smooth_chain, blend_smooth_chain_if_closed};
762pub(crate) use corner::round_concave_chain_corner;
763pub use corner::round_convex_corner;
764pub use edge::{blend_closed_edge, blend_closed_edge_knot_aware, blend_edge_variable, blend_open_edge, is_pcurve_floor_refusal, with_pcurve_knot_retry};
765pub(crate) use collapse::is_full_width_collapse;
766pub(crate) use planar_chart::{
767    fit_planar_charts_to_trims, is_planar_chart_refusal, planar_chart_overrun,
768};
769pub(crate) use track_fit::PCURVE_OFF_FLOOR;
770pub(crate) use edge::{consumed_band, is_consumed_snap, is_rail_collapse};
771pub(crate) use network::{
772    blend_star_network, blend_star_network_laws, blend_star_network_with_corner_policy, degenerate_corner_setbacks, mixed_convexity_corner, mixed_convexity_corners, selection_convexity, DegenerateSetback,
773    MixedCorner,
774};
775pub(crate) use miter::{is_marched_fit_off_carriers, is_miter_support_overrun};
776/// The two faces each selected edge lies on, in the network's mate order
777/// (the open-pad composition reads which is planar and which is the boss).
778pub(crate) fn selection_mates<'a>(
779    solid: &'a crate::topology::BrepSolid,
780    edge_ids: &[u64],
781    radius: f64,
782) -> Result<Vec<(&'a crate::topology::EdgeRecord, [&'a crate::topology::FaceRecord; 2])>, crate::KernelRefusal> {
783    Ok(network::stripe_mates(solid, edge_ids, radius)?
784        .into_iter()
785        .map(|(edge, first, second)| (edge, [first.face, second.face]))
786        .collect())
787}
788
789pub(crate) use runout::{build_runout_walls, plan_runout, take_runout_reports, RunoutPlan, RunoutWallReport};
790pub(crate) use fold::{is_wall_fold, wall_loop_refusal, WALL_FOLDS};
791pub(crate) use stations::is_ball_off_carrier;
792// Lane D 2026-10-14 (appended): the inside-corner classification's name and
793// its opt-in miter switch, read by the runout composition and its tests.
794pub(crate) use network::is_reentrant_concave;
795
796/// Off the switch every closed rim's two support pcurves are refitted to their
797/// rail's projected track (`edge/closed.rs::refit_closed_support_pcurve`, the
798/// closed chain's support-piece fitter: a dense track of analytic feet, a
799/// verified fit ladder and the shipped read), 96 of the half torus's 98 ms of
800/// rows. On the switch each trim is first READ as shipped at the same floor
801/// (`track_fit::read_shipped_pcurve`, `PCURVE_REFINEMENT_TOLERANCE`): a plane
802/// mate's trim is the rail's own control points through the plane's affine
803/// chart (exact by construction, the exact revolution lane's rule); any other
804/// mate's trim, or an image that misses, takes the refit as before (the
805/// march's own interpolated pcurve was measured as a candidate and dropped:
806/// 8.5e-5 off on the half torus's torus mate). The shipped contract is
807/// unchanged (every trim read within the floor); the bytes are not, so it
808/// landed OFF and was promoted by its census (the kernel battery, the case
809/// manifest and hlr_tests with the switch on). ON by default;
810/// `BREP_BLEND_CLOSED_TRIM_READ_FIRST=0` is the escape hatch back to the refit
811/// road, `=1` forces it on; `set_closed_trim_read_first_for_test` forces
812/// either way on a test's thread.
813const CLOSED_TRIM_READ_FIRST_DEFAULT: bool = true;
814
815thread_local! {
816    static CLOSED_TRIM_READ_FIRST_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
817}
818
819pub(crate) fn closed_trim_read_first_on() -> bool {
820    if let Some(forced) = CLOSED_TRIM_READ_FIRST_FOR_TEST.with(|forced| forced.get()) {
821        return forced;
822    }
823    match std::env::var("BREP_BLEND_CLOSED_TRIM_READ_FIRST").as_deref() {
824        Ok("0") => false,
825        Ok("1") => true,
826        _ => CLOSED_TRIM_READ_FIRST_DEFAULT,
827    }
828}
829
830
831/// The check solves 31 centres per interval by continuation from the
832/// neighbour, each a full station Newton with a finite-difference Jacobian
833/// rebuilt at every iteration (eleven evaluations of the tangency system a
834/// centre, 89 of the half torus's 169 ms of march). On the switch each centre
835/// is seeded by a quadratic extrapolation through the last three and solved
836/// by the chord method with the Jacobian carried from centre to centre
837/// (`stations::solve_station_state_chord`), to the plain solver's own stop;
838/// the plain solver from the neighbour is the fall-back for anything the
839/// chord cannot finish. The centres are the same roots to the same tolerance,
840/// not the same bits: the declared wall reading moves in its last digits, its
841/// verdict and the body do not, which is why it landed OFF and was promoted
842/// by its census. ON by default; `BREP_BLEND_CLOSED_WALL_PREDICT=0` is the
843/// escape hatch back to the plain continuation solves, `=1` forces it on;
844/// `set_closed_wall_predict_for_test` forces either way on a test's thread.
845const CLOSED_WALL_PREDICT_DEFAULT: bool = true;
846
847thread_local! {
848    static CLOSED_WALL_PREDICT_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
849}
850
851pub(crate) fn closed_wall_predict_on() -> bool {
852    if let Some(forced) = CLOSED_WALL_PREDICT_FOR_TEST.with(|forced| forced.get()) {
853        return forced;
854    }
855    match std::env::var("BREP_BLEND_CLOSED_WALL_PREDICT").as_deref() {
856        Ok("0") => false,
857        Ok("1") => true,
858        _ => CLOSED_WALL_PREDICT_DEFAULT,
859    }
860}
861
862
863// BREP lane L: begin keyway_floor_open_mixed_corner_20261014
864pub(crate) use corner::{open_mixed_corner_on, round_mixed_concave_open_corner};
865// BREP lane L: end keyway_floor_open_mixed_corner_20261014
866