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 OPEN_PAD_NETWORK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
57}
58
59pub(crate) fn with_open_pad_network<T>(build: impl FnOnce() -> T) -> T {
71 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
83pub(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
104const 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
122const 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
139const 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
160const 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
178const EXACT_LANE_DEFAULT: bool = true;
186
187const 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
214const 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
235const 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
263const 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
278const 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
295const 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 Stops(Vec<(f64, f64)>),
317}
318
319impl StripeRadius {
320 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 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 pub(crate) fn constant(&self) -> Option<f64> {
355 match self {
356 StripeRadius::Constant(radius) => Some(*radius),
357 StripeRadius::Stops(_) => None,
358 }
359 }
360
361 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 pub(crate) fn end(&self, at_start: bool) -> f64 {
373 self.at_fraction(if at_start { 0.0 } else { 1.0 })
374 }
375
376 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
392pub(crate) fn record_blend_note(note: String) {
397 BLEND_NOTES.with(|notes| notes.borrow_mut().push(note));
398}
399
400pub fn take_blend_notes() -> Vec<String> {
404 BLEND_NOTES.with(|notes| std::mem::take(&mut *notes.borrow_mut()))
405}
406
407pub(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#[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 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
441pub(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
474pub(crate) mod timing {
484 use std::cell::{Cell, RefCell};
485 use web_time::Instant;
486
487 #[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 pub fn blend_timing_capture(on: bool) {
506 CAPTURE.with(|capture| capture.set(on));
507 }
508
509 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 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 pub(crate) fn record_ms(measure: &str, ms: f64) {
562 if enabled() {
563 add(measure, ms, 1);
564 }
565 }
566
567 pub(crate) fn record_count(measure: &str, n: u64) {
569 if enabled() {
570 add(measure, 0.0, n);
571 }
572 }
573
574 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 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 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
612pub(crate) fn record_wall_model_report(report: WallModelReport) {
617 BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().push(report));
618}
619
620
621pub(crate) fn blend_approximations_pending() -> bool {
624 BLEND_APPROXIMATIONS.with(|reports| !reports.borrow().is_empty())
625}
626
627pub(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
644fn 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
660pub 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 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};
776pub(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;
792pub(crate) use network::is_reentrant_concave;
795
796const 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
831const 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
863pub(crate) use corner::{open_mixed_corner_on, round_mixed_concave_open_corner};
865