1mod 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
41pub(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 static EXACT_LANE_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
55 static EXACT_REVOLUTION_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
56 static EXACT_RIM_LANE_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
57 static ACCEPT_ONCE_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
58 static CHAIN_ARC_STATIONS_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
59 static TAPER_NETWORK_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
60 static ROLLOVER_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
61 static EXACT_TRIM_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
62 static UNEQUAL_MITER_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
63 static NO_COMMON_BALL_STAR_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
64 static CROSSED_RAILS_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
65}
66
67const CROSSED_RAILS_DEFAULT: bool = true;
73
74pub(crate) fn crossed_rails_on() -> bool {
75 if let Some(forced) = CROSSED_RAILS_FOR_TEST.with(|forced| forced.get()) {
76 return forced;
77 }
78 match std::env::var("BREP_BLEND_CROSSED_RAILS").as_deref() {
79 Ok("0") => false,
80 Ok("1") => true,
81 _ => CROSSED_RAILS_DEFAULT,
82 }
83}
84
85const NO_COMMON_BALL_STAR_DEFAULT: bool = true;
90
91pub(crate) fn no_common_ball_star_on() -> bool {
92 if let Some(forced) = NO_COMMON_BALL_STAR_FOR_TEST.with(|forced| forced.get()) {
93 return forced;
94 }
95 match std::env::var("BREP_BLEND_RIBBON_STAR").as_deref() {
96 Ok("0") => false,
97 Ok("1") => true,
98 _ => NO_COMMON_BALL_STAR_DEFAULT,
99 }
100}
101
102const UNEQUAL_MITER_DEFAULT: bool = true;
111
112pub(crate) fn unequal_miter_on() -> bool {
113 if let Some(forced) = UNEQUAL_MITER_FOR_TEST.with(|forced| forced.get()) {
114 return forced;
115 }
116 match std::env::var("BREP_BLEND_UNEQUAL_MITER").as_deref() {
117 Ok("0") => false,
118 Ok("1") => true,
119 _ => UNEQUAL_MITER_DEFAULT,
120 }
121}
122
123const ROLLOVER_DEFAULT: bool = true;
129
130pub(crate) fn rollover_on() -> bool {
131 if let Some(forced) = ROLLOVER_FOR_TEST.with(|forced| forced.get()) {
132 return forced;
133 }
134 match std::env::var("BREP_BLEND_ROLLOVER").as_deref() {
135 Ok("0") => false,
136 Ok("1") => true,
137 _ => ROLLOVER_DEFAULT,
138 }
139}
140
141const EXACT_LANE_DEFAULT: bool = true;
149
150const EXACT_RIM_LANE_DEFAULT: bool = true;
154
155pub(crate) fn exact_rim_lane_on() -> bool {
156 if let Some(forced) = EXACT_RIM_LANE_FOR_TEST.with(|forced| forced.get()) {
157 return forced;
158 }
159 match std::env::var("BREP_BLEND_EXACT_RIM").as_deref() {
160 Ok("0") => false,
161 Ok("1") => true,
162 _ => EXACT_RIM_LANE_DEFAULT,
163 }
164}
165
166pub(crate) fn exact_lane_on() -> bool {
167 if let Some(forced) = EXACT_LANE_FOR_TEST.with(|forced| forced.get()) {
168 return forced;
169 }
170 match std::env::var("BREP_BLEND_EXACT_LANE").as_deref() {
171 Ok("0") => false,
172 Ok("1") => true,
173 _ => EXACT_LANE_DEFAULT,
174 }
175}
176
177const EXACT_REVOLUTION_DEFAULT: bool = true;
186
187pub(crate) fn exact_revolution_on() -> bool {
188 if let Some(forced) = EXACT_REVOLUTION_FOR_TEST.with(|forced| forced.get()) {
189 return forced;
190 }
191 match std::env::var("BREP_BLEND_EXACT_REVOLUTION").as_deref() {
192 Ok("0") => false,
193 Ok("1") => true,
194 _ => EXACT_REVOLUTION_DEFAULT,
195 }
196}
197
198const EXACT_TRIM_DEFAULT: bool = true;
214
215pub(crate) fn exact_trim_on() -> bool {
216 if let Some(forced) = EXACT_TRIM_FOR_TEST.with(|forced| forced.get()) {
217 return forced;
218 }
219 match std::env::var("BREP_BLEND_EXACT_TRIM").as_deref() {
220 Ok("0") => false,
221 Ok("1") => true,
222 _ => EXACT_TRIM_DEFAULT,
223 }
224}
225
226const ACCEPT_ONCE_DEFAULT: bool = true;
229
230pub(crate) fn accept_once_on() -> bool {
231 if let Some(forced) = ACCEPT_ONCE_FOR_TEST.with(|forced| forced.get()) {
232 return forced;
233 }
234 match std::env::var("BREP_BLEND_ACCEPT_ONCE").as_deref() {
235 Ok("0") => false,
236 Ok("1") => true,
237 _ => ACCEPT_ONCE_DEFAULT,
238 }
239}
240
241const CHAIN_ARC_STATIONS_DEFAULT: bool = false;
246
247pub(crate) fn chain_arc_stations_on() -> bool {
248 if let Some(forced) = CHAIN_ARC_STATIONS_FOR_TEST.with(|forced| forced.get()) {
249 return forced;
250 }
251 match std::env::var("BREP_BLEND_CHAIN_ARC_STATIONS").as_deref() {
252 Ok("0") => false,
253 Ok("1") => true,
254 _ => CHAIN_ARC_STATIONS_DEFAULT,
255 }
256}
257
258const TAPER_NETWORK_DEFAULT: bool = true;
261
262pub(crate) fn taper_network_on() -> bool {
263 if let Some(forced) = TAPER_NETWORK_FOR_TEST.with(|forced| forced.get()) {
264 return forced;
265 }
266 match std::env::var("BREP_BLEND_TAPER_NETWORK").as_deref() {
267 Ok("0") => false,
268 Ok("1") => true,
269 _ => TAPER_NETWORK_DEFAULT,
270 }
271}
272
273#[derive(Clone, Debug, PartialEq)]
274pub(crate) enum StripeRadius {
275 Constant(f64),
276 Stops(Vec<(f64, f64)>),
280}
281
282impl StripeRadius {
283 pub(crate) fn from_stops(stops: &[(f64, f64)]) -> Self {
286 let mut sorted = stops.to_vec();
287 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
288 match sorted.first() {
289 Some(&(_, first)) if sorted.iter().all(|(_, r)| (r - first).abs() <= 1e-12 * (1.0 + first.abs())) => {
290 StripeRadius::Constant(first)
291 }
292 _ => StripeRadius::Stops(sorted),
293 }
294 }
295
296 pub(crate) fn at_fraction(&self, fraction: f64) -> f64 {
298 match self {
299 StripeRadius::Constant(radius) => *radius,
300 StripeRadius::Stops(stops) => {
301 let fraction = fraction.clamp(0.0, 1.0);
302 if fraction <= stops[0].0 {
303 return stops[0].1;
304 }
305 for pair in stops.windows(2) {
306 if fraction <= pair[1].0 {
307 let width = (pair[1].0 - pair[0].0).max(1e-12);
308 return pair[0].1 + (pair[1].1 - pair[0].1) * (fraction - pair[0].0) / width;
309 }
310 }
311 stops[stops.len() - 1].1
312 }
313 }
314 }
315
316 pub(crate) fn constant(&self) -> Option<f64> {
318 match self {
319 StripeRadius::Constant(radius) => Some(*radius),
320 StripeRadius::Stops(_) => None,
321 }
322 }
323
324 pub(crate) fn largest(&self) -> f64 {
326 match self {
327 StripeRadius::Constant(radius) => *radius,
328 StripeRadius::Stops(stops) => stops.iter().map(|(_, r)| *r).fold(0.0, f64::max),
329 }
330 }
331
332 pub(crate) fn end(&self, at_start: bool) -> f64 {
336 self.at_fraction(if at_start { 0.0 } else { 1.0 })
337 }
338
339 pub(crate) fn is_positive(&self) -> bool {
341 match self {
342 StripeRadius::Constant(radius) => radius.is_finite() && *radius > 0.0,
343 StripeRadius::Stops(stops) => !stops.is_empty() && stops.iter().all(|(_, r)| r.is_finite() && *r > 0.0),
344 }
345 }
346}
347
348
349
350
351
352
353
354
355pub(crate) fn record_blend_note(note: String) {
360 BLEND_NOTES.with(|notes| notes.borrow_mut().push(note));
361}
362
363pub fn take_blend_notes() -> Vec<String> {
367 BLEND_NOTES.with(|notes| std::mem::take(&mut *notes.borrow_mut()))
368}
369
370pub(crate) fn blend_notes_mark() -> usize {
373 BLEND_NOTES.with(|notes| notes.borrow().len())
374}
375
376pub(crate) fn blend_notes_truncate(mark: usize) {
377 BLEND_NOTES.with(|notes| notes.borrow_mut().truncate(mark));
378}
379
380#[derive(Clone, Debug)]
385pub(crate) struct WallModelReport {
386 pub(crate) name: Option<String>,
387 pub(crate) surface: crate::NurbsSurface,
388 pub(crate) request: f64,
389 pub(crate) residual: f64,
390 pub(crate) detail: Option<String>,
393 pub(crate) budget: crate::ApproximationBudget,
394}
395
396thread_local! {
397 static BLEND_APPROXIMATIONS: std::cell::RefCell<Vec<WallModelReport>> = const { std::cell::RefCell::new(Vec::new()) };
398}
399
400thread_local! {
401 static BLEND_OPERATION_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
402}
403
404pub(crate) struct BlendOperation;
412
413impl BlendOperation {
414 pub(crate) fn enter() -> BlendOperation {
415 BLEND_OPERATION_DEPTH.with(|depth| {
416 if depth.get() == 0 {
417 BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().clear());
418 timing::begin_operation();
419 }
420 depth.set(depth.get() + 1);
421 });
422 BlendOperation
423 }
424}
425
426impl Drop for BlendOperation {
427 fn drop(&mut self) {
428 BLEND_OPERATION_DEPTH.with(|depth| {
429 depth.set(depth.get().saturating_sub(1));
430 if depth.get() == 0 {
431 timing::end_operation();
432 }
433 });
434 }
435}
436
437pub(crate) mod timing {
447 use std::cell::{Cell, RefCell};
448 use web_time::Instant;
449
450 #[derive(Clone, Debug, PartialEq)]
453 pub struct BlendTiming {
454 pub measure: String,
455 pub ms: f64,
456 pub count: u64,
457 }
458
459 thread_local! {
460 static CAPTURE: Cell<bool> = const { Cell::new(false) };
461 static ENABLED: Cell<bool> = const { Cell::new(false) };
462 static TO_STDERR: Cell<bool> = const { Cell::new(false) };
463 static LEDGER: RefCell<Vec<BlendTiming>> = const { RefCell::new(Vec::new()) };
464 }
465
466 pub fn blend_timing_capture(on: bool) {
469 CAPTURE.with(|capture| capture.set(on));
470 }
471
472 pub fn take_blend_timings() -> Vec<BlendTiming> {
475 let mut entries = LEDGER.with(|ledger| std::mem::take(&mut *ledger.borrow_mut()));
476 entries.sort_by(|a, b| a.measure.cmp(&b.measure));
477 entries
478 }
479
480 pub(crate) fn enabled() -> bool {
482 ENABLED.with(|enabled| enabled.get())
483 }
484
485 pub(super) fn begin_operation() {
486 let profile = std::env::var_os("BREP_PROFILE").is_some();
487 TO_STDERR.with(|to_stderr| to_stderr.set(profile));
488 let on = profile || CAPTURE.with(|capture| capture.get());
489 ENABLED.with(|enabled| enabled.set(on));
490 if on {
491 LEDGER.with(|ledger| ledger.borrow_mut().clear());
492 }
493 }
494
495 pub(super) fn end_operation() {
496 if TO_STDERR.with(|to_stderr| to_stderr.get()) {
497 let mut entries = LEDGER.with(|ledger| ledger.borrow().clone());
498 entries.sort_by(|a, b| a.measure.cmp(&b.measure));
499 for entry in entries {
500 if entry.measure.starts_with("timing.") {
501 eprintln!("blend.{}={:.3} n={}", entry.measure, entry.ms, entry.count);
502 } else {
503 eprintln!("blend.{}={}", entry.measure, entry.count);
504 }
505 }
506 }
507 ENABLED.with(|enabled| enabled.set(false));
508 }
509
510 fn add(measure: &str, ms: f64, count: u64) {
511 LEDGER.with(|ledger| {
512 let mut ledger = ledger.borrow_mut();
513 match ledger.iter_mut().find(|entry| entry.measure == measure) {
514 Some(entry) => {
515 entry.ms += ms;
516 entry.count += count;
517 }
518 None => ledger.push(BlendTiming { measure: measure.to_string(), ms, count }),
519 }
520 });
521 }
522
523 pub(crate) fn record_ms(measure: &str, ms: f64) {
525 if enabled() {
526 add(measure, ms, 1);
527 }
528 }
529
530 pub(crate) fn record_count(measure: &str, n: u64) {
532 if enabled() {
533 add(measure, 0.0, n);
534 }
535 }
536
537 pub(crate) struct BlendTimer {
540 measure: &'static str,
541 started: Option<Instant>,
542 }
543
544 impl BlendTimer {
545 pub(crate) fn start(measure: &'static str) -> BlendTimer {
546 BlendTimer { measure, started: enabled().then(Instant::now) }
547 }
548
549 pub(crate) fn elapsed_ms(&self) -> f64 {
551 self.started.map_or(0.0, |started| started.elapsed().as_secs_f64() * 1_000.0)
552 }
553
554 pub(crate) fn stop(mut self) -> f64 {
556 let ms = self.elapsed_ms();
557 if self.started.take().is_some() {
558 add(self.measure, ms, 1);
559 }
560 ms
561 }
562 }
563
564 impl Drop for BlendTimer {
565 fn drop(&mut self) {
566 if let Some(started) = self.started.take() {
567 add(self.measure, started.elapsed().as_secs_f64() * 1_000.0, 1);
568 }
569 }
570 }
571}
572pub use timing::{blend_timing_capture, take_blend_timings, BlendTiming};
573pub(crate) use timing::{record_count, BlendTimer};
574
575pub(crate) fn record_wall_model_report(report: WallModelReport) {
580 BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().push(report));
581}
582
583
584pub(crate) fn blend_approximations_pending() -> bool {
587 BLEND_APPROXIMATIONS.with(|reports| !reports.borrow().is_empty())
588}
589
590pub(crate) fn same_surface(a: &crate::NurbsSurface, b: &crate::NurbsSurface) -> bool {
593 let bits = |values: &[f64]| values.iter().map(|value| value.to_bits()).collect::<Vec<_>>();
594 a.degree_u == b.degree_u
595 && a.degree_v == b.degree_v
596 && bits(&a.knots_u) == bits(&b.knots_u)
597 && bits(&a.knots_v) == bits(&b.knots_v)
598 && a.control_points.len() == b.control_points.len()
599 && a.control_points.iter().zip(&b.control_points).all(|(row_a, row_b)| {
600 row_a.len() == row_b.len()
601 && row_a.iter().zip(row_b).all(|(p, q)| {
602 [p.x, p.y, p.z, p.w].map(f64::to_bits) == [q.x, q.y, q.z, q.w].map(f64::to_bits)
603 })
604 })
605}
606
607fn names_the_wall(face_name: &Option<String>, report_name: &Option<String>) -> bool {
611 match (face_name, report_name) {
612 (Some(face), Some(report)) => {
613 face == report
614 || face.strip_prefix(report.as_str()).and_then(|rest| rest.strip_prefix('[')).and_then(|rest| rest.strip_suffix(']')).is_some_and(
615 |index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()),
616 )
617 }
618 (None, None) => true,
619 _ => false,
620 }
621}
622
623pub fn take_blend_approximations(bodies: &[(&str, &crate::BrepSolid)]) -> Vec<crate::Approximation> {
636 let snapshot = BLEND_APPROXIMATIONS.with(|reports| std::mem::take(&mut *reports.borrow_mut()));
637 let mut unique: Vec<WallModelReport> = Vec::new();
638 for report in snapshot {
639 unique.retain(|kept| !(kept.name == report.name && same_surface(&kept.surface, &report.surface)));
640 unique.push(report);
641 }
642 let mut approximations = Vec::new();
643 for (body, solid) in bodies {
644 for report in &unique {
645 let carried = solid.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
646 names_the_wall(&face.name, &report.name) && same_surface(&face.surface, &report.surface)
647 });
648 if carried {
649 let wall = report.name.as_deref().unwrap_or("(unnamed)");
650 let unaccepted = matches!(report.budget.reason, crate::BudgetReason::Unaccepted | crate::BudgetReason::Unread);
651 let budget = match report.budget.mechanism {
652 crate::BudgetMechanism::LocalRounds => format!(
653 "local refinement stopped: {:?} after {} of {} rounds, {} of {} stations per segment",
654 report.budget.reason, report.budget.rounds_used, report.budget.rounds_limit,
655 report.budget.stations, report.budget.station_limit,
656 ),
657 crate::BudgetMechanism::StationRungs => format!(
658 "the open march's station ladder stopped: {:?} after {} of {} global station rungs, {} of {} stations",
659 report.budget.reason, report.budget.rounds_used, report.budget.rounds_limit,
660 report.budget.stations, report.budget.station_limit,
661 ),
662 };
663 let unknown = report.budget.unread > 0;
664 let rails = report.budget.measured_component == crate::MeasuredComponent::Rails;
665 let mut message = if !unaccepted && (rails || unknown) {
666 let what = if rails {
670 format!("the fillet wall {wall}'s support rails stand")
671 } else {
672 format!("the fillet wall {wall} stands")
673 };
674 let against = if rails {
675 "off their exact construction (their carriers / the exact ball contacts)"
676 } else {
677 "from the rolling ball between its rails"
678 };
679 let reading = if unknown {
680 format!(
681 "an UNKNOWN distance {against}: {} interval(s) could not be read (the largest READABLE reading is {:.3e} mm, not the worst)",
682 report.budget.unread, report.residual
683 )
684 } else {
685 format!("{:.3e} mm {against}", report.residual)
686 };
687 format!(
688 "{what} {reading}, against a construction request of {:.3e} mm (accepted at the intersection-fit bar); {budget}",
689 report.request,
690 )
691 } else if unaccepted {
692 format!(
693 "the blend wall {wall} ships from a rung that never met acceptance: its worst reading \
694 {:.3e} mm against its acceptance bar {:.3e} mm; {budget}",
695 report.residual, report.request,
696 )
697 } else {
698 format!(
699 "the fillet wall {wall} stands {:.3e} mm from the rolling ball between its rails, against a \
700 construction request of {:.3e} mm (accepted at the intersection-fit bar); {budget}",
701 report.residual, report.request,
702 )
703 };
704 if let Some(detail) = &report.detail {
705 message = format!("{message} ({detail})");
706 }
707 approximations.push(crate::Approximation {
708 code: if unaccepted { "blend.wall_acceptance" } else { "blend.wall_model" }.to_string(),
709 body: body.to_string(),
710 measured: report.residual,
711 bar: report.request,
712 volume_bound: None,
713 edges: Vec::new(),
714 budget: Some(report.budget.clone()),
715 message,
716 });
717 }
718 }
719 }
720 approximations
721}
722
723
724pub use chain::{blend_smooth_chain, blend_smooth_chain_if_closed};
725pub(crate) use corner::round_concave_chain_corner;
726pub use corner::round_convex_corner;
727pub use edge::{blend_closed_edge, blend_edge_variable, blend_open_edge};
728pub(crate) use collapse::is_full_width_collapse;
729pub(crate) use planar_chart::{
730 fit_planar_charts_to_trims, is_planar_chart_refusal, planar_chart_overrun,
731};
732pub(crate) use track_fit::PCURVE_OFF_FLOOR;
733pub(crate) use edge::{consumed_band, is_consumed_snap, is_rail_collapse};
734pub(crate) use network::{
735 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,
736 MixedCorner,
737};
738pub(crate) use miter::{is_marched_fit_off_carriers, is_miter_support_overrun};
739pub(crate) use runout::{build_runout_walls, plan_runout, take_runout_reports, RunoutPlan, RunoutWallReport};
740pub(crate) use fold::{is_wall_fold, WALL_FOLDS};
741pub(crate) use stations::is_ball_off_carrier;