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(crate) mod runout;
31mod stations;
32mod rows;
33mod track_fit;
34
35thread_local! {
36    static REFINE_OFF_FOR_TEST: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
37}
38
39/// Whether the open march's local station refinement runs. Escape hatch
40/// `BREP_BLEND_REFINE=0` restores the uniform ladder exactly; a test turns it
41/// off for its own thread with `set_refinement_off_for_test`.
42pub(crate) fn station_refinement_on() -> bool {
43    std::env::var("BREP_BLEND_REFINE").as_deref() != Ok("0")
44        && !REFINE_OFF_FOR_TEST.with(|off| off.get())
45}
46
47thread_local! {
48    static BLEND_NOTES: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
49}
50
51/// Record what a blend that SHIPS needs its feature to say about it — today,
52/// a march whose rails still stand off their carriers after its station
53/// ladder and local refinement ran out. Drained per feature by the history
54/// loop ([`take_blend_notes`]), beside the soundness acceptance's repairs.
55pub(crate) fn record_blend_note(note: String) {
56    BLEND_NOTES.with(|notes| notes.borrow_mut().push(note));
57}
58
59/// Take every blend note recorded on this thread since the last call.
60/// Blending runs on the feature's own thread (no parallel march), so the
61/// history loop's drain sees every note its feature recorded.
62pub fn take_blend_notes() -> Vec<String> {
63    BLEND_NOTES.with(|notes| std::mem::take(&mut *notes.borrow_mut()))
64}
65
66/// The ledger's length, and a way back to it: a lane that refuses takes back
67/// the notes it recorded, so a note never describes a wall that did not ship.
68pub(crate) fn blend_notes_mark() -> usize {
69    BLEND_NOTES.with(|notes| notes.borrow().len())
70}
71
72pub(crate) fn blend_notes_truncate(mark: usize) {
73    BLEND_NOTES.with(|notes| notes.borrow_mut().truncate(mark));
74}
75
76/// A fillet chain wall that SHIPPED short of the construction request its
77/// accepted rung made (`KernelTolerances::model` on the wall between its
78/// rails): which wall — the blend face's name and its exact surface — what
79/// was asked, what the wall reads, and the typed budget that ran out.
80#[derive(Clone, Debug)]
81pub(crate) struct WallModelReport {
82    pub(crate) name: Option<String>,
83    pub(crate) surface: crate::NurbsSurface,
84    pub(crate) request: f64,
85    pub(crate) residual: f64,
86    /// What else the lane measured that the message must say (an unaccepted
87    /// rung's deficit, a ladder rung's local refinement), verbatim.
88    pub(crate) detail: Option<String>,
89    pub(crate) budget: crate::ApproximationBudget,
90}
91
92thread_local! {
93    static BLEND_APPROXIMATIONS: std::cell::RefCell<Vec<WallModelReport>> = const { std::cell::RefCell::new(Vec::new()) };
94}
95
96thread_local! {
97    static BLEND_OPERATION_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
98}
99
100/// One blend OPERATION's scope on this thread: every public blend entry point
101/// (and the history loop, around each feature) holds one. The OUTERMOST
102/// entry drops whatever an earlier operation recorded and nobody consumed —
103/// a direct API caller consumes ([`take_blend_approximations`]) after the
104/// operation whose reports it wants, before starting the next — while a
105/// nested entry (a fillet's per-edge chain blend) clears nothing, so every
106/// report of the operation in flight is kept, however many there are.
107pub(crate) struct BlendOperation;
108
109impl BlendOperation {
110    pub(crate) fn enter() -> BlendOperation {
111        BLEND_OPERATION_DEPTH.with(|depth| {
112            if depth.get() == 0 {
113                BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().clear());
114            }
115            depth.set(depth.get() + 1);
116        });
117        BlendOperation
118    }
119}
120
121impl Drop for BlendOperation {
122    fn drop(&mut self) {
123        BLEND_OPERATION_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
124    }
125}
126
127/// Record a wall that ships with its request unmet. Written by the rung that
128/// keeps its body; whether the body reaches a caller is decided at
129/// consumption ([`take_blend_approximations`]), not here. Every report of
130/// the operation in flight is kept (see [`BlendOperation`]).
131pub(crate) fn record_wall_model_report(report: WallModelReport) {
132    BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().push(report));
133}
134
135
136/// Whether any wall-model report is waiting (the history loop reads the
137/// returned bodies only then).
138pub(crate) fn blend_approximations_pending() -> bool {
139    BLEND_APPROXIMATIONS.with(|reports| !reports.borrow().is_empty())
140}
141
142/// Bit-identical surfaces: the same degrees, knots and homogeneous control
143/// points, compared by their bits.
144pub(crate) fn same_surface(a: &crate::NurbsSurface, b: &crate::NurbsSurface) -> bool {
145    let bits = |values: &[f64]| values.iter().map(|value| value.to_bits()).collect::<Vec<_>>();
146    a.degree_u == b.degree_u
147        && a.degree_v == b.degree_v
148        && bits(&a.knots_u) == bits(&b.knots_u)
149        && bits(&a.knots_v) == bits(&b.knots_v)
150        && a.control_points.len() == b.control_points.len()
151        && a.control_points.iter().zip(&b.control_points).all(|(row_a, row_b)| {
152            row_a.len() == row_b.len()
153                && row_a.iter().zip(row_b).all(|(p, q)| {
154                    [p.x, p.y, p.z, p.w].map(f64::to_bits) == [q.x, q.y, q.z, q.w].map(f64::to_bits)
155                })
156        })
157}
158
159/// Whether `face_name` names the report's wall: its exact name, or that
160/// name with the `[k]` (decimal) suffix `ensure_unique_face_names` gives a
161/// colliding name at registration. Nothing else: no prefix, no other suffix.
162fn names_the_wall(face_name: &Option<String>, report_name: &Option<String>) -> bool {
163    match (face_name, report_name) {
164        (Some(face), Some(report)) => {
165            face == report
166                || face.strip_prefix(report.as_str()).and_then(|rest| rest.strip_prefix('[')).and_then(|rest| rest.strip_suffix(']')).is_some_and(
167                    |index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()),
168                )
169        }
170        (None, None) => true,
171        _ => false,
172    }
173}
174
175/// Consume EVERY wall-model report recorded on this thread since the last
176/// call, in ONE snapshot, against ALL the bodies a call returned
177/// (`(body name, solid)`): a report becomes a typed `blend.wall_model`
178/// [`crate::Approximation`] on each body carrying its wall — a face with a
179/// bit-identical surface AND the report's name, exactly or as registration
180/// renamed it (`NAME[k]`) — and is dropped when no returned body carries it
181/// (a lane that refused, a body a later step discarded). A wall carried by
182/// two returned bodies (one surface, bit for bit, in both) is reported on
183/// both: the reading is a property of that surface. Reports for one wall
184/// collapse to the last. Never a match by name alone.
185/// `take_blend_approximations(&[])` discards everything recorded (the
186/// history loop's stale drain before a feature runs).
187pub fn take_blend_approximations(bodies: &[(&str, &crate::BrepSolid)]) -> Vec<crate::Approximation> {
188    let snapshot = BLEND_APPROXIMATIONS.with(|reports| std::mem::take(&mut *reports.borrow_mut()));
189    let mut unique: Vec<WallModelReport> = Vec::new();
190    for report in snapshot {
191        unique.retain(|kept| !(kept.name == report.name && same_surface(&kept.surface, &report.surface)));
192        unique.push(report);
193    }
194    let mut approximations = Vec::new();
195    for (body, solid) in bodies {
196        for report in &unique {
197            let carried = solid.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
198                names_the_wall(&face.name, &report.name) && same_surface(&face.surface, &report.surface)
199            });
200            if carried {
201                let wall = report.name.as_deref().unwrap_or("(unnamed)");
202                let unaccepted = matches!(report.budget.reason, crate::BudgetReason::Unaccepted | crate::BudgetReason::Unread);
203                let budget = match report.budget.mechanism {
204                    crate::BudgetMechanism::LocalRounds => format!(
205                        "local refinement stopped: {:?} after {} of {} rounds, {} of {} stations per segment",
206                        report.budget.reason, report.budget.rounds_used, report.budget.rounds_limit,
207                        report.budget.stations, report.budget.station_limit,
208                    ),
209                    crate::BudgetMechanism::StationRungs => format!(
210                        "the open march's station ladder stopped: {:?} after {} of {} global station rungs, {} of {} stations",
211                        report.budget.reason, report.budget.rounds_used, report.budget.rounds_limit,
212                        report.budget.stations, report.budget.station_limit,
213                    ),
214                };
215                let unknown = report.budget.unread > 0;
216                let rails = report.budget.measured_component == crate::MeasuredComponent::Rails;
217                let mut message = if !unaccepted && (rails || unknown) {
218                    // A request that covers the rails and the wall says WHICH
219                    // it measured, and never offers a partial reading as the
220                    // worst: with unread intervals the worst is unknown.
221                    let what = if rails {
222                        format!("the fillet wall {wall}'s support rails stand")
223                    } else {
224                        format!("the fillet wall {wall} stands")
225                    };
226                    let against = if rails {
227                        "off their exact construction (their carriers / the exact ball contacts)"
228                    } else {
229                        "from the rolling ball between its rails"
230                    };
231                    let reading = if unknown {
232                        format!(
233                            "an UNKNOWN distance {against}: {} interval(s) could not be read (the largest READABLE reading is {:.3e} mm, not the worst)",
234                            report.budget.unread, report.residual
235                        )
236                    } else {
237                        format!("{:.3e} mm {against}", report.residual)
238                    };
239                    format!(
240                        "{what} {reading}, against a construction request of {:.3e} mm (accepted at the intersection-fit bar); {budget}",
241                        report.request,
242                    )
243                } else if unaccepted {
244                    format!(
245                        "the blend wall {wall} ships from a rung that never met acceptance: its worst reading \
246                         {:.3e} mm against its acceptance bar {:.3e} mm; {budget}",
247                        report.residual, report.request,
248                    )
249                } else {
250                    format!(
251                        "the fillet wall {wall} stands {:.3e} mm from the rolling ball between its rails, against a \
252                         construction request of {:.3e} mm (accepted at the intersection-fit bar); {budget}",
253                        report.residual, report.request,
254                    )
255                };
256                if let Some(detail) = &report.detail {
257                    message = format!("{message} ({detail})");
258                }
259                approximations.push(crate::Approximation {
260                    code: if unaccepted { "blend.wall_acceptance" } else { "blend.wall_model" }.to_string(),
261                    body: body.to_string(),
262                    measured: report.residual,
263                    bar: report.request,
264                    volume_bound: None,
265                    edges: Vec::new(),
266                    budget: Some(report.budget.clone()),
267                    message,
268                });
269            }
270        }
271    }
272    approximations
273}
274
275
276pub use chain::{blend_smooth_chain, blend_smooth_chain_if_closed};
277pub(crate) use corner::round_concave_chain_corner;
278pub use corner::round_convex_corner;
279pub use edge::{blend_closed_edge, blend_edge_variable, blend_open_edge};
280pub(crate) use collapse::is_full_width_collapse;
281pub(crate) use planar_chart::{
282    fit_planar_charts_to_trims, is_planar_chart_refusal, planar_chart_overrun,
283};
284pub(crate) use track_fit::PCURVE_OFF_FLOOR;
285pub(crate) use edge::{consumed_band, is_consumed_snap, is_rail_collapse};
286pub(crate) use network::{
287    blend_star_network, blend_star_network_with_corner_policy, degenerate_corner_setbacks, mixed_convexity_corner, mixed_convexity_corners, selection_convexity, DegenerateSetback,
288    MixedCorner,
289};
290pub(crate) use miter::{is_marched_fit_off_carriers, is_miter_support_overrun};
291pub(crate) use runout::{build_runout_walls, plan_runout, take_runout_reports, RunoutPlan, RunoutWallReport};
292pub(crate) use fold::{is_wall_fold, WALL_FOLDS};
293pub(crate) use stations::is_ball_off_carrier;