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