Skip to main content

brep_kernel/offset/
point.rs

1//! The ONE definition of "the offset of a surface at a parameter".
2//!
3//! Push-face, offset-shell, thicken and the blend march are all surface
4//! *offsetting* problems, and each of them had written the same three lines by
5//! hand: evaluate the carrier, take a unit normal, step `distance` along it.
6//! The audit records four independent copies; a fifth (`blend/edge/keep.rs`)
7//! and two more in offset-shell turned up while this module was written. They
8//! are collected here.
9//!
10//! # What this module is NOT
11//!
12//! It is **not** [`crate::offset_surface`]. That function is a Greville *fit*:
13//! it samples the offset at the source basis's Greville parameters and
14//! re-interpolates. Two independent reasons a fit is the wrong shared seam are
15//! recorded in-tree — handing a fitted surface to the blend march's Newton loop
16//! would inject the fit's approximation error into a residual whose bar is
17//! `1e-11·(1+scale)` (`blending/blend/stations.rs`), and
18//! `edit/direct_edit/face_offset_sphere.rs` records that `offset_surface` was
19//! tried for the full-sphere push and *failed*, because the Greville fit
20//! degenerates at the poles and the result stops re-recognising as a sphere.
21//! The shared foundation is the **pointwise evaluator**; `offset_surface` is one
22//! of its consumers (a fit of it), not the other way round.
23//!
24//! # The sign convention
25//!
26//! There is exactly ONE here: **`distance` is signed ALONG the returned
27//! normal.** `point = source + distance · normal`. The kernel's other
28//! convention — `offset_surface`'s "positive distance moves *opposite* the
29//! face's outward normal" — is expressed by its adapter negating on the way in,
30//! at one labelled place, instead of by four unlabelled hand negations
31//! (audit §4.1).
32//!
33//! # The orientation conventions
34//!
35//! Orientation is an explicit parameter, never read off a `FaceRecord`, because
36//! the callers genuinely disagree (audit §4.2) and both readings are
37//! load-bearing. [`OffsetNormal`] has exactly the three that exist in the tree,
38//! and each is defined to be *bit-identical* to the formula it replaces — see
39//! its variant docs.
40
41use crate::{AnalyticSurface, NurbsSurface, Vec3};
42
43/// Which unit normal the offset rides, and how it is recovered where the
44/// parametric normal degenerates.
45///
46/// Every variant names an existing in-tree formula and is bit-identical to it.
47/// They differ ONLY where the parameter leaves the domain or the cross product
48/// `S_u × S_v` collapses; a shared evaluator that silently picked one for
49/// everybody would move behaviour at exactly the singular points the callers
50/// each decided about on purpose.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum OffsetNormal {
53    /// The bare `S_u × S_v`, normalized, through the C¹ domain extension
54    /// (`deriv1_extended`). Orientation is the *caller's* business — the blend
55    /// march carries it in the sign of ρ instead (`signed_radii`), which is why
56    /// this variant must not apply `same_sense`.
57    ///
58    /// Not [`OffsetNormal::FaceStable`]'s recovery — no nudge off a point and
59    /// no walk along a collapsed row. Where the cross product vanishes this
60    /// reads the ONE-SIDED LIMIT of the same cross product from inside the
61    /// domain ([`raw_unit_normal`]), the surface twin of
62    /// `NurbsCurve::unit_tangent`: an extruded face inherits a stationary
63    /// parameter of its profile curve as an isoline where `S_u` vanishes,
64    /// though the face is perfectly regular there. Only a carrier with no
65    /// regular point in reach refuses, by name. The POINT past such a boundary
66    /// moves along the same limit ([`raw_extended_point`]) where the ruled
67    /// extension would stand still. Near such an isoline the vanishing partial
68    /// is re-read from the control net's differences ([`well_rounded_partials`]),
69    /// whose rounding shrinks with it, so the normal keeps its direction up to
70    /// the isoline instead of turning with the basis derivatives' rounding.
71    ///
72    /// Bit-identical to `blending::blend::stations::raw_normal` (and to
73    /// `evaluate_extended` for the point) wherever `S_u × S_v` is non-zero.
74    Raw,
75    /// `NurbsSurface::normal` — the in-domain `derivatives(u, v, 1)` cross —
76    /// negated when `same_sense` is false. Clamped, not extended, and no
77    /// singular recovery.
78    ///
79    /// Bit-identical to the `let mut normal = surface.normal(u, v)?; if
80    /// !face.same_sense { normal = normal.scale(-1.0) }` block written out in
81    /// `face_offset_revolution.rs`, `face_offset_freeform.rs`,
82    /// `face_offset_torus.rs` and `offset_shell/smooth_sync.rs::face_normal`.
83    Face { same_sense: bool },
84    /// [`OffsetNormal::Face`] plus the singular-row recovery that
85    /// `offset_surface` depends on: nudge off a point where the normal is
86    /// undefined, and at a *collapsed parameter row* (a cone apex) walk deep
87    /// inward for the true per-ruling limit rather than accept the axis
88    /// direction the at-point cross degenerates to.
89    ///
90    /// Bit-identical to the former `offset::stable_face_normal`, whose body
91    /// this is. The blend march must NOT inherit it — the recovery would
92    /// change the residual at poles, so it stays opt-in (audit slice 1,
93    /// "keep it as an opt-in flag").
94    FaceStable { same_sense: bool },
95    /// The exact closed-form normal of the *recognised analytic carrier*,
96    /// oriented like [`OffsetNormal::Face`].
97    ///
98    /// This is the "exact per analytic surface type" lane: a sphere's normal is
99    /// `(p − centre)/r` — defined at the poles, where `S_u × S_v` vanishes — a
100    /// cylinder's is its radial direction, a cone's is the meridian
101    /// perpendicular (defined even AT the apex, from the ruling's own azimuth),
102    /// a torus's is `(p − tube centre)/r`. Where there is no exact form (the
103    /// general `Revolution`, and every free-form patch) it falls back to
104    /// [`OffsetNormal::Face`], so this variant is always at least as defined as
105    /// that one.
106    ///
107    /// `NurbsSurface::analytic` memoizes per instance, so recognition is paid
108    /// once per surface, not once per point.
109    ///
110    /// **No consumer rides this lane yet.** It exists as the exact half of the
111    /// evaluator's contract and as the diagnostic's comparator; switching a
112    /// caller onto it changes that caller's last digits and is a separate,
113    /// measured step. See [`offset_normal_diagnostic`].
114    ExactAnalytic { same_sense: bool },
115}
116
117impl OffsetNormal {
118    /// The `same_sense`-carrying variants, for a caller that has a face.
119    fn same_sense(self) -> bool {
120        match self {
121            OffsetNormal::Raw => true,
122            OffsetNormal::Face { same_sense }
123            | OffsetNormal::FaceStable { same_sense }
124            | OffsetNormal::ExactAnalytic { same_sense } => same_sense,
125        }
126    }
127}
128
129/// One evaluation of the offset: the source point, the unit normal the offset
130/// rides, and the offset point itself.
131///
132/// The normal is also the *offset surface's own* normal wherever the offset is
133/// regular — it flips only past the evolute, which is what the region-scoped
134/// scan in `offset/regularity.rs` (the kernel's only curvature gate, shared by
135/// `thicken`'s sheet refusal and offset-shell's carrier-collapse lane) exists
136/// to refuse, over the trim rather than over the whole carrier. This struct
137/// deliberately carries no other derivative data: the
138/// five consumers were audited and not one of them uses `S_u`/`S_v` for
139/// anything but the normal, and the blend march's Jacobian is finite-differenced
140/// on the residual rather than assembled from partials.
141#[derive(Clone, Copy, Debug)]
142pub struct OffsetSample {
143    /// `S(u, v)` on the source carrier.
144    pub source: Vec3,
145    /// The unit normal, in the requested orientation.
146    pub normal: Vec3,
147    /// `source + distance · normal`.
148    pub point: Vec3,
149}
150
151/// The pointwise offset evaluator, bound to one carrier and one orientation
152/// convention.
153///
154/// Construction is free — it stores three references and reads nothing — so a
155/// caller inside a Newton loop may build one per call without paying anything.
156/// `site` is a short stable label used only by [`offset_normal_diagnostic`].
157#[derive(Clone, Copy)]
158pub struct OffsetEvaluator<'a> {
159    site: &'static str,
160    surface: &'a NurbsSurface,
161    convention: OffsetNormal,
162}
163
164impl<'a> OffsetEvaluator<'a> {
165    pub fn new(site: &'static str, surface: &'a NurbsSurface, convention: OffsetNormal) -> Self {
166        Self {
167            site,
168            surface,
169            convention,
170        }
171    }
172
173    /// The unit normal alone, for callers that only need the direction (a
174    /// residual check, or an affine offset that shifts the whole control net by
175    /// one vector). Evaluates the point only on the lanes that need it.
176    pub fn normal(&self, u: f64, v: f64) -> Result<Vec3, String> {
177        let result = self.normal_with(self.convention, u, v);
178        offset_normal_diagnostic(self.site, self.surface, self.convention, u, v, &result);
179        result
180    }
181
182    /// Whether the carrier evaluated at `(u, v)` is the REAL surface, or its
183    /// fiction — the question [`Self::at`] cannot answer for itself.
184    ///
185    /// `OffsetNormal::Raw` evaluates through `deriv1_extended`, which
186    /// continues an OPEN direction along its boundary tangent plane without
187    /// bound instead of refusing. A solve with no solution on the real
188    /// carrier therefore converges on that continuation and reports a
189    /// residual of zero, because the extended point IS a genuine root of the
190    /// extended system. Callers must ask here, at convergence.
191    ///
192    /// Being outside the domain is NOT by itself the fault, and testing for
193    /// that alone over-refuses badly. Three cases:
194    ///
195    /// * a CLOSED direction wraps (`rem_euclid`) back onto the real surface,
196    ///   so it is always faithful — which is why a full cylinder cannot
197    ///   exhibit the problem and a trimmed arc can;
198    /// * an open direction along which the surface is RULED extends exactly.
199    ///   A plane is a bounded bilinear patch in this kernel, and its
200    ///   continuation is still that plane; a cylinder's axial direction
201    ///   continues as the same cylinder. A re-entrant corner touching a
202    ///   wall's carrier beyond its patch is real geometry and must keep
203    ///   working;
204    /// * an open direction with non-zero NORMAL CURVATURE is where the ruled
205    ///   continuation leaves the surface, and that is the fiction.
206    ///
207    /// So the test is geometric, not a domain comparison and not keyed on
208    /// [`NurbsSurface::analytic`] — an analytic-keyed guard silently no-ops on
209    /// every fitted patch. The ruled extension drops the second-order term, so
210    /// over an excursion `d` it departs the true surface by about
211    /// `½·d²·|S_ii·n|`, where `S_ii·n` is the second-fundamental-form
212    /// coefficient along the exceeded direction (Golovanov §4.12's `b11`/`b22`,
213    /// the same quantities the marcher's step budget uses). Faithful when that
214    /// departure is within `tolerance`.
215    ///
216    /// The bound is QUADRATIC in the excursion, so a parameter a rounding bit
217    /// past the boundary is faithful by many orders and a real excursion onto
218    /// a curved carrier is unfaithful by many orders. The exact threshold
219    /// barely matters; passing the caller's own convergence bar keeps the
220    /// answer honest to the accuracy it claims.
221    pub fn on_real_carrier(&self, u: f64, v: f64, tolerance: f64) -> Result<bool, String> {
222        let [u0, u1] = self.surface.domain_u()?;
223        let [v0, v1] = self.surface.domain_v()?;
224        let (closed_u, closed_v) = self.surface.closed_directions()?;
225        let out = |value: f64, lo: f64, hi: f64, closed: bool| -> f64 {
226            if closed {
227                0.0
228            } else if value < lo {
229                value - lo
230            } else if value > hi {
231                value - hi
232            } else {
233                0.0
234            }
235        };
236        let du_out = out(u, u0, u1, closed_u);
237        let dv_out = out(v, v0, v1, closed_v);
238        if du_out == 0.0 && dv_out == 0.0 {
239            return Ok(true);
240        }
241        let derivatives = self.surface.derivatives(u - du_out, v - dv_out, 2)?;
242        let normal = match derivatives[1][0].cross(derivatives[0][1]).normalized() {
243            Ok(normal) => normal,
244            // A degenerate anchor: see `degenerate_anchor_on_real_carrier`.
245            Err(_) => {
246                return self.degenerate_anchor_on_real_carrier(u, v, du_out, dv_out, tolerance)
247            }
248        };
249        let departure = |step: f64, second: Vec3| 0.5 * step * step * second.dot(normal).abs();
250        if du_out != 0.0 && departure(du_out, derivatives[2][0]) > tolerance {
251            return Ok(false);
252        }
253        if dv_out != 0.0 && departure(dv_out, derivatives[0][2]) > tolerance {
254            return Ok(false);
255        }
256        Ok(true)
257    }
258
259    /// [`Self::on_real_carrier`] where `S_u × S_v` vanishes at the anchor.
260    ///
261    /// Where the anchor lies on a STATIONARY boundary isoline
262    /// ([`stationary_boundary`]) that the point has left across, the carrier is
263    /// regular and has a normal (the `Raw` lane's one-sided limit), and the
264    /// continuation past it is [`raw_extended_point`]'s: straight along the
265    /// isoline's limit direction, `speed·|excursion|` long. The second-order
266    /// departure `½·d²·|S_ii·n|` cannot measure it, because `S_ii` is the
267    /// parameterization's, not the carrier's. So the departure is measured on
268    /// the carrier itself: the point the same distance back INSIDE the patch,
269    /// through the [`StationaryChart`], sits off the boundary's tangent plane by
270    /// what a regular continuation would depart from the straight one, to
271    /// leading order (the mirror image of `½·κ_n·s²`). Faithful when that is
272    /// within `tolerance`. An excursion longer than the patch mirrors to its far
273    /// boundary and scales that departure by the square of the ratio.
274    ///
275    /// Every other degenerate anchor — a pole, an apex, a collapsed row — has no
276    /// normal to measure against, and the faithfulness of its extension cannot
277    /// be established, so it answers `false`, as it always has. A surface that
278    /// does not leave its boundary to any order refuses by name
279    /// ([`Self::stationary_chart`]).
280    fn degenerate_anchor_on_real_carrier(
281        &self,
282        u: f64,
283        v: f64,
284        du_out: f64,
285        dv_out: f64,
286        tolerance: f64,
287    ) -> Result<bool, String> {
288        let (anchor_u, anchor_v) = (u - du_out, v - dv_out);
289        let charts = [
290            (du_out != 0.0)
291                .then(|| self.stationary_chart(true, anchor_u, anchor_v))
292                .transpose()?
293                .flatten(),
294            (dv_out != 0.0)
295                .then(|| self.stationary_chart(false, anchor_u, anchor_v))
296                .transpose()?
297                .flatten(),
298        ];
299        if charts.iter().all(Option::is_none) {
300            return Ok(false);
301        }
302        let (anchor_point, su, sv) = self.surface.deriv1(anchor_u, anchor_v)?;
303        let Ok(normal) = raw_unit_normal(self.surface, anchor_u, anchor_v, su, sv) else {
304            return Ok(false);
305        };
306        let derivatives = self.surface.derivatives(anchor_u, anchor_v, 2)?;
307        let ([u0, u1], [v0, v1]) = domains(self.surface)?;
308        for (along_u, out, chart) in [(true, du_out, charts[0]), (false, dv_out, charts[1])] {
309            if out == 0.0 {
310                continue;
311            }
312            let departure = match chart {
313                Some(chart) => {
314                    let excursion = chart.regular(if along_u { u } else { v });
315                    let (lo, hi) = if along_u { (u0, u1) } else { (v0, v1) };
316                    let far = if chart.outward > 0.0 { lo } else { hi };
317                    // The patch mirrors at most back to its far boundary; a longer
318                    // excursion scales that departure by the square of the ratio,
319                    // the order a regular continuation departs at.
320                    let reach = -chart.regular(far);
321                    let (inside, growth) = if excursion <= reach {
322                        (chart.parameter(-excursion), 1.0)
323                    } else {
324                        (far, (excursion / reach).powi(2))
325                    };
326                    let mirrored = if along_u {
327                        self.surface.evaluate(inside, anchor_v)?
328                    } else {
329                        self.surface.evaluate(anchor_u, inside)?
330                    };
331                    growth * mirrored.sub(anchor_point).dot(normal).abs()
332                }
333                None => {
334                    let second = if along_u { derivatives[2][0] } else { derivatives[0][2] };
335                    0.5 * out * out * second.dot(normal).abs()
336                }
337            };
338            if departure > tolerance {
339                return Ok(false);
340            }
341        }
342        Ok(true)
343    }
344
345    /// The offset of this carrier at `(u, v)` by `distance` **along the
346    /// normal** (see the module's sign convention).
347    pub fn at(&self, u: f64, v: f64, distance: f64) -> Result<OffsetSample, String> {
348        let (source, normal) = self.evaluate(u, v)?;
349        Ok(OffsetSample {
350            source,
351            normal,
352            point: source.add(normal.scale(distance)),
353        })
354    }
355
356    fn evaluate(&self, u: f64, v: f64) -> Result<(Vec3, Vec3), String> {
357        let result = match self.convention {
358            // ONE evaluation for the point and the partials the normal needs —
359            // `deriv1_extended`'s point is bit-identical to `evaluate_extended`,
360            // which is what licenses `blend/edge/keep.rs` to drop its second
361            // evaluation of the same surface.
362            OffsetNormal::Raw => self
363                .surface
364                .deriv1_extended(u, v)
365                .and_then(|(point, su, sv)| {
366                    let normal = raw_unit_normal(self.surface, u, v, su, sv)?;
367                    Ok((raw_extended_point(self.surface, u, v, point, su, sv)?, normal))
368                }),
369            convention => self
370                .surface
371                .evaluate(u, v)
372                .and_then(|point| Ok((point, self.normal_with(convention, u, v)?))),
373        };
374        let normal = result
375            .as_ref()
376            .map(|(_, normal)| *normal)
377            .map_err(Clone::clone);
378        offset_normal_diagnostic(self.site, self.surface, self.convention, u, v, &normal);
379        result
380    }
381
382    fn normal_with(&self, convention: OffsetNormal, u: f64, v: f64) -> Result<Vec3, String> {
383        match convention {
384            OffsetNormal::Raw => {
385                let (_, su, sv) = self.surface.deriv1_extended(u, v)?;
386                raw_unit_normal(self.surface, u, v, su, sv)
387            }
388            OffsetNormal::Face { same_sense } => {
389                Ok(orient(self.surface.normal(u, v)?, same_sense))
390            }
391            OffsetNormal::FaceStable { same_sense } => {
392                stable_normal(self.surface, same_sense, u, v)
393            }
394            OffsetNormal::ExactAnalytic { same_sense } => {
395                let point = self.surface.evaluate(u, v)?;
396                match exact_analytic_normal(self.surface, u, v, point)? {
397                    Some(normal) => Ok(orient(normal, same_sense)),
398                    None => Ok(orient(self.surface.normal(u, v)?, same_sense)),
399                }
400            }
401        }
402    }
403}
404
405fn orient(normal: Vec3, same_sense: bool) -> Vec3 {
406    if same_sense {
407        normal
408    } else {
409        normal.scale(-1.0)
410    }
411}
412
413fn domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), String> {
414    Ok((surface.domain_u()?, surface.domain_v()?))
415}
416
417/// The `Raw` lane's unit normal from the partials `deriv1_extended` returned at
418/// `(u, v)`: `S_u × S_v` normalized — the same vector, to the bit, wherever it
419/// is non-zero — or its one-sided limit where it vanishes.
420///
421/// Why a limit and not a refusal: a surface can be stationary in its
422/// PARAMETERIZATION where its geometry is regular. The standard source is an
423/// extrusion (or any ruled face) of a profile curve whose first derivative
424/// vanishes at a vertex — the involute flank's Hermite fit, a cubic with two
425/// coincident poles — which carries that parameter as a whole isoline where
426/// `S_u = 0`. The blend march evaluates its support's normal on exactly that
427/// isoline when it rolls along the rim edge, and the face's normal is still the
428/// limit from inside, as a curve's direction of travel is at its cusp
429/// (`NurbsCurve::unit_tangent`, whose ladder this mirrors). The limit is also
430/// the side the march's orientation came from: `signed_radii` reads the sign of
431/// ρ at a regular interior point, and a one-sided probe from inside the domain
432/// keeps that sense, where a probe past a stationary boundary would not exist.
433///
434/// The probe origin is the in-domain parameter `deriv1_extended` anchors on —
435/// wrapped in a closed direction, clamped past an open boundary — and the
436/// ladder walks first along the parameter whose partial collapsed, then along
437/// the other (a pole row collapses `S_u` along the whole row, so only a step in
438/// `v` leaves it). Only a carrier with no regular point within a tenth of its
439/// domain refuses, and the refusal names where.
440///
441/// `BREP_CUSP_TANGENT_RESCUE=0` restores the bare `normalized()` refusal, the
442/// same hatch the curve tangent carries.
443fn raw_unit_normal(
444    surface: &NurbsSurface,
445    u: f64,
446    v: f64,
447    su: Vec3,
448    sv: Vec3,
449) -> Result<Vec3, String> {
450    let (su, sv) = well_rounded_partials(surface, u, v, su, sv)?;
451    let error = match su.cross(sv).normalized() {
452        Ok(normal) => return Ok(normal),
453        Err(error) => error,
454    };
455    if std::env::var("BREP_CUSP_TANGENT_RESCUE").as_deref() == Ok("0") {
456        return Err(error);
457    }
458    let ([u0, u1], [v0, v1]) = domains(surface)?;
459    let (closed_u, closed_v) = surface.closed_directions()?;
460    let anchor = |value: f64, lo: f64, hi: f64, closed: bool| -> f64 {
461        if closed && (value < lo || value > hi) {
462            lo + (value - lo).rem_euclid(hi - lo)
463        } else {
464            value.clamp(lo, hi)
465        }
466    };
467    let (anchor_u, anchor_v) = (anchor(u, u0, u1, closed_u), anchor(v, v0, v1, closed_v));
468    let along_u_first = su.length() <= sv.length();
469    let directions = if along_u_first {
470        [(1.0, 0.0), (0.0, 1.0)]
471    } else {
472        [(0.0, 1.0), (1.0, 0.0)]
473    };
474    for scale in [1e-6, 1e-4, 1e-2, 1e-1] {
475        for (along_u, along_v) in directions {
476            let step_u = (u1 - u0) * scale * along_u;
477            let step_v = (v1 - v0) * scale * along_v;
478            for sign in [1.0, -1.0] {
479                let probe_u = anchor_u + sign * step_u;
480                let probe_v = anchor_v + sign * step_v;
481                if probe_u < u0 || probe_u > u1 || probe_v < v0 || probe_v > v1 {
482                    continue;
483                }
484                if let Some(normal) = surface
485                    .deriv1_extended(probe_u, probe_v)
486                    .and_then(|(_, su, sv)| {
487                        well_rounded_partials(surface, probe_u, probe_v, su, sv)
488                    })
489                    .ok()
490                    .and_then(|(su, sv)| su.cross(sv).normalized().ok())
491                {
492                    return Ok(normal);
493                }
494            }
495        }
496    }
497    Err(format!(
498        "offset normal: S_u x S_v vanishes at (u, v) = ({u}, {v}) and at every probe the \
499         one-sided limit can reach in [{u0}, {u1}] x [{v0}, {v1}], so this carrier has no \
500         normal to report there"
501    ))
502}
503
504/// The `Raw` lane's POINT past an open boundary: `deriv1_extended`'s point —
505/// the same point, to the bit — unless the partial ACROSS that boundary
506/// vanishes at the anchor, where the ruled extension `S(anchor) + d·S_u` does
507/// not move at all.
508///
509/// That is the stationary isoline [`raw_unit_normal`] reads its limit on: an
510/// extrusion of a profile whose cubic has coincident end poles is regular up
511/// to and across the boundary, and only its parameterization stops there. A
512/// solve that has to leave the domain across such a boundary — the blend
513/// march's overshoot stations, which seat the rolling ball past a vertex —
514/// then meets a Jacobian column that is exactly zero ("station Newton is
515/// singular"), though the continuation of the face exists and is the one its
516/// cusp-free twin extends onto. So the extension moves along the one-sided
517/// limit of the vanished partial's DIRECTION, probed from inside the domain on
518/// the ladder `raw_unit_normal` uses, at the isoline's mean speed across its
519/// parameter range, `|S(hi, ·) − S(lo, ·)| / (hi − lo)`.
520///
521/// The speed is a choice the geometry does not make — the continuation's
522/// locus is fixed by the direction alone — and this one is intrinsic to the
523/// carrier: on a ruled face over a straight profile it is the speed the
524/// cusp-free twin's own partial has, so the solved parameter past the boundary
525/// is the twin's.
526///
527/// Refuses by name only where the surface itself gives no direction: the
528/// vanished partial is zero at every probe the ladder reaches, a patch
529/// collapsed across a tenth of its domain, where `raw_unit_normal` refuses
530/// too. `BREP_CUSP_TANGENT_RESCUE=0` restores the stationary extension.
531fn raw_extended_point(
532    surface: &NurbsSurface,
533    u: f64,
534    v: f64,
535    point: Vec3,
536    su: Vec3,
537    sv: Vec3,
538) -> Result<Vec3, String> {
539    let ([u0, u1], [v0, v1]) = domains(surface)?;
540    if u >= u0 && u <= u1 && v >= v0 && v <= v1 {
541        return Ok(point);
542    }
543    if su.normalized().is_ok() && sv.normalized().is_ok() {
544        return Ok(point);
545    }
546    if std::env::var("BREP_CUSP_TANGENT_RESCUE").as_deref() == Ok("0") {
547        return Ok(point);
548    }
549    let (closed_u, closed_v) = surface.closed_directions()?;
550    let excursion = |value: f64, lo: f64, hi: f64, closed: bool| -> f64 {
551        if closed {
552            0.0
553        } else if value < lo {
554            value - lo
555        } else if value > hi {
556            value - hi
557        } else {
558            0.0
559        }
560    };
561    let du_out = excursion(u, u0, u1, closed_u);
562    let dv_out = excursion(v, v0, v1, closed_v);
563    let wrap = |value: f64, lo: f64, hi: f64, closed: bool| -> f64 {
564        if closed && (value < lo || value > hi) {
565            lo + (value - lo).rem_euclid(hi - lo)
566        } else {
567            value.clamp(lo, hi)
568        }
569    };
570    let (anchor_u, anchor_v) = (wrap(u, u0, u1, closed_u), wrap(v, v0, v1, closed_v));
571    let mut point = point;
572    for (along_u, out, partial) in [(true, du_out, su), (false, dv_out, sv)] {
573        if out == 0.0 || partial.normalized().is_ok() {
574            continue;
575        }
576        let (lo, hi) = if along_u { (u0, u1) } else { (v0, v1) };
577        let inward = -out.signum();
578        let direction = [1e-6, 1e-4, 1e-2, 1e-1].iter().find_map(|scale| {
579            let step = inward * (hi - lo) * scale;
580            let (probe_u, probe_v) = if along_u {
581                (anchor_u + step, anchor_v)
582            } else {
583                (anchor_u, anchor_v + step)
584            };
585            if probe_u < u0 || probe_u > u1 || probe_v < v0 || probe_v > v1 {
586                return None;
587            }
588            let (_, probe_su, probe_sv) = surface.deriv1(probe_u, probe_v).ok()?;
589            let (probe_su, probe_sv) =
590                well_rounded_partials(surface, probe_u, probe_v, probe_su, probe_sv).ok()?;
591            (if along_u { probe_su } else { probe_sv }).normalized().ok()
592        });
593        let Some(direction) = direction else {
594            return Err(format!(
595                "offset point: S_{} vanishes on the boundary of [{u0}, {u1}] x [{v0}, {v1}] at \
596                 (u, v) = ({anchor_u}, {anchor_v}) and at every probe the one-sided limit can \
597                 reach, so the surface has no direction to extend along toward ({u}, {v})",
598                if along_u { "u" } else { "v" }
599            ));
600        };
601        let speed = isoline_mean_speed(surface, along_u, anchor_u, anchor_v)?;
602        point = point.add(direction.scale(out * speed));
603    }
604    Ok(point)
605}
606
607/// The mean speed of the isoline through `(anchor_u, anchor_v)` along `along_u`
608/// across its whole parameter range, `|S(hi, ·) − S(lo, ·)| / (hi − lo)`: the
609/// speed [`raw_extended_point`] moves a point past a stationary boundary at, and
610/// the one [`StationaryChart`] joins it with.
611fn isoline_mean_speed(
612    surface: &NurbsSurface,
613    along_u: bool,
614    anchor_u: f64,
615    anchor_v: f64,
616) -> Result<f64, String> {
617    let ([u0, u1], [v0, v1]) = domains(surface)?;
618    let (lo, hi) = if along_u { (u0, u1) } else { (v0, v1) };
619    let (start, end) = if along_u {
620        (surface.evaluate(lo, anchor_v)?, surface.evaluate(hi, anchor_v)?)
621    } else {
622        (surface.evaluate(anchor_u, lo)?, surface.evaluate(anchor_u, hi)?)
623    };
624    Ok(end.sub(start).length() / (hi - lo))
625}
626
627/// The in-domain anchor `deriv1_extended` reads for `(u, v)`: wrapped in a
628/// closed direction, clamped past an open boundary.
629fn extension_anchor(surface: &NurbsSurface, u: f64, v: f64) -> Result<(f64, f64), String> {
630    let ([u0, u1], [v0, v1]) = domains(surface)?;
631    let (closed_u, closed_v) = surface.closed_directions()?;
632    let anchor = |value: f64, lo: f64, hi: f64, closed: bool| -> f64 {
633        if closed && (value < lo || value > hi) {
634            lo + (value - lo).rem_euclid(hi - lo)
635        } else {
636            value.clamp(lo, hi)
637        }
638    };
639    Ok((anchor(u, u0, u1, closed_u), anchor(v, v0, v1, closed_v)))
640}
641
642/// The open boundary nearest `(u, v)` in the direction `along_u`, when the
643/// partial ACROSS it vanishes there while the partial ALONG it does not: an
644/// isoline on which only the parameterization is stationary, the extrusion of a
645/// profile cubic with coincident end poles. `None` in a closed direction, past
646/// a regular boundary, and on a collapsed row (a pole or an apex, where the
647/// partial along the boundary vanishes too, so the boundary is a point and not
648/// an isoline).
649///
650/// The zero test is `Vec3::normalized`'s, the one [`raw_unit_normal`] and
651/// [`raw_extended_point`] already apply.
652fn stationary_boundary(
653    surface: &NurbsSurface,
654    along_u: bool,
655    u: f64,
656    v: f64,
657) -> Result<Option<f64>, String> {
658    let ([u0, u1], [v0, v1]) = domains(surface)?;
659    let (closed_u, closed_v) = surface.closed_directions()?;
660    if (along_u && closed_u) || (!along_u && closed_v) {
661        return Ok(None);
662    }
663    let (anchor_u, anchor_v) = extension_anchor(surface, u, v)?;
664    let (value, lo, hi) = if along_u { (u, u0, u1) } else { (v, v0, v1) };
665    let boundary = if (value - lo).abs() <= (hi - value).abs() { lo } else { hi };
666    let (at_u, at_v) = if along_u { (boundary, anchor_v) } else { (anchor_u, boundary) };
667    let (_, su, sv) = surface.deriv1(at_u, at_v)?;
668    let (across, along) = if along_u { (su, sv) } else { (sv, su) };
669    Ok((across.normalized().is_err() && along.normalized().is_ok()).then_some(boundary))
670}
671
672/// Below this fraction of the other partial's reach over its domain, a partial
673/// is small enough that its rounding may be its direction: only then is its
674/// boundary asked whether it is stationary. A gate on cost, not on the answer —
675/// the answer is [`stationary_boundary`]'s — and the bare partial's tilt at the
676/// gate is about ulp·|S|/|S_u| ≈ 1e-12 of a unit model, so switching there moves
677/// a normal by no more than that.
678const STATIONARY_PARTIAL_REACH: f64 = 1e-3;
679
680/// The partials `(S_u, S_v)` the `Raw` lane builds its normal from: the ones
681/// `deriv1_extended` returned — the same vectors, to the bit — unless one of
682/// them is small against the other and belongs to a [`stationary_boundary`],
683/// where it is re-read by [`NurbsSurface::partial_by_differences`] at the
684/// in-domain anchor.
685///
686/// Why: near such an isoline the partial vanishes like the distance to it, and
687/// the basis-derivative evaluation's rounding does not, so the normal
688/// `S_u × S_v` turns by `ulp·|S|/|S_u|`. The tombstone fixtures measured 3.5e-11
689/// at `|S_u| = 7.7e-5` and 3.7e-5 at `|S_u| = 1.7e-10`, and the one-sided limit's
690/// own probe (a millionth of the span in) 2.2e-10: all above the blend march's
691/// residual bar of `1e-11·(1 + scale)`, and inconsistent with each other across
692/// the isoline, so the station Newton at a stationary vertex could not settle.
693/// The difference form's rounding scales with the partial, so the direction
694/// survives to the isoline itself.
695///
696/// `BREP_CUSP_TANGENT_RESCUE=0` keeps the bare partials.
697fn well_rounded_partials(
698    surface: &NurbsSurface,
699    u: f64,
700    v: f64,
701    su: Vec3,
702    sv: Vec3,
703) -> Result<(Vec3, Vec3), String> {
704    let ([u0, u1], [v0, v1]) = domains(surface)?;
705    let reach_u = su.length_squared() * (u1 - u0) * (u1 - u0);
706    let reach_v = sv.length_squared() * (v1 - v0) * (v1 - v0);
707    let gate = STATIONARY_PARTIAL_REACH * STATIONARY_PARTIAL_REACH;
708    let small_u = reach_u <= gate * reach_v;
709    let small_v = reach_v <= gate * reach_u;
710    if !small_u && !small_v {
711        return Ok((su, sv));
712    }
713    if std::env::var("BREP_CUSP_TANGENT_RESCUE").as_deref() == Ok("0") {
714        return Ok((su, sv));
715    }
716    let (anchor_u, anchor_v) = extension_anchor(surface, u, v)?;
717    let mut partials = (su, sv);
718    if small_u && stationary_boundary(surface, true, u, v)?.is_some() {
719        if let Ok(partial) = surface.partial_by_differences(true, anchor_u, anchor_v) {
720            partials.0 = partial;
721        }
722    }
723    if small_v && stationary_boundary(surface, false, u, v)?.is_some() {
724        if let Ok(partial) = surface.partial_by_differences(false, anchor_u, anchor_v) {
725            partials.1 = partial;
726        }
727    }
728    Ok(partials)
729}
730
731/// A REGULAR parameter for one direction of a carrier near an open boundary on
732/// which that direction's partial vanishes ([`stationary_boundary`]).
733///
734/// There the carrier is regular and only its parameterization stops: the point
735/// leaves the boundary like `L·d^m` in the outward parameter distance `d`
736/// (`m` the order of the first non-vanishing partial across the boundary, 2 for
737/// a cubic with two coincident end poles). A Newton that solves for the
738/// parameter itself therefore meets a root of multiplicity `m` wherever its
739/// answer lies ON the isoline — the blend march's station at a stationary
740/// vertex — and converges linearly on a Jacobian column that vanishes with
741/// the step.
742///
743/// The chart's value `w` is
744///
745/// * `−L·|d|^m` inside the domain (`d ≤ 0`), which is the distance from the
746///   boundary along the carrier to leading order, so the point moves at unit
747///   speed in `w` right up to the isoline;
748/// * `speed·d` past it, where [`raw_extended_point`] moves the point along the
749///   one-sided limit at the isoline's mean speed — so in `w` the continuation
750///   also moves at unit speed, and the chart joins it C¹ at `w = 0`.
751///
752/// On a straight isoline the point is affine in `w` on both sides to the order
753/// of the carrier's next term; the Newton's column in `w` no longer vanishes.
754#[derive(Clone, Copy, Debug)]
755pub(crate) struct StationaryChart {
756    boundary: f64,
757    /// +1 when the boundary is the high end of the domain, −1 the low end.
758    outward: f64,
759    order: i32,
760    /// `L = |∂^m S| / m!` across the boundary.
761    coefficient: f64,
762    speed: f64,
763}
764
765impl StationaryChart {
766    /// The chart value of the carrier parameter `parameter`.
767    pub(crate) fn regular(&self, parameter: f64) -> f64 {
768        let outward = self.outward * (parameter - self.boundary);
769        if outward >= 0.0 {
770            self.speed * outward
771        } else {
772            -self.coefficient * (-outward).powi(self.order)
773        }
774    }
775
776    /// The carrier parameter of the chart value `regular`.
777    pub(crate) fn parameter(&self, regular: f64) -> f64 {
778        let outward = if regular >= 0.0 {
779            regular / self.speed
780        } else if self.order == 2 {
781            -(-regular / self.coefficient).sqrt()
782        } else {
783            -(-regular / self.coefficient).powf(1.0 / f64::from(self.order))
784        };
785        self.boundary + self.outward * outward
786    }
787}
788
789impl OffsetEvaluator<'_> {
790    /// The [`StationaryChart`] for the direction `along_u` at `(u, v)`, when that
791    /// direction's nearest open boundary is a stationary isoline; `None`
792    /// otherwise, and on every lane but `Raw`, whose point past the boundary is
793    /// what the chart joins.
794    ///
795    /// Refuses by name only where the surface itself is singular: the partial
796    /// across the boundary vanishes to every order the carrier's degree
797    /// reaches, so the patch does not leave the boundary at all.
798    /// `BREP_CUSP_TANGENT_RESCUE=0` answers `None`.
799    pub(crate) fn stationary_chart(
800        &self,
801        along_u: bool,
802        u: f64,
803        v: f64,
804    ) -> Result<Option<StationaryChart>, String> {
805        if self.convention != OffsetNormal::Raw
806            || std::env::var("BREP_CUSP_TANGENT_RESCUE").as_deref() == Ok("0")
807        {
808            return Ok(None);
809        }
810        let surface = self.surface;
811        let Some(boundary) = stationary_boundary(surface, along_u, u, v)? else {
812            return Ok(None);
813        };
814        let ([u0, u1], [v0, v1]) = domains(surface)?;
815        let hi = if along_u { u1 } else { v1 };
816        let (anchor_u, anchor_v) = extension_anchor(surface, u, v)?;
817        let (at_u, at_v) = if along_u { (boundary, anchor_v) } else { (anchor_u, boundary) };
818        let degree = if along_u { surface.degree_u } else { surface.degree_v };
819        let derivatives = surface.derivatives(at_u, at_v, degree)?;
820        let mut factorial = 1.0;
821        let mut found = None;
822        for order in 2..=degree {
823            factorial *= order as f64;
824            let partial = if along_u {
825                derivatives[order][0]
826            } else {
827                derivatives[0][order]
828            };
829            if partial.normalized().is_ok() {
830                found = Some((order, partial.length() / factorial));
831                break;
832            }
833        }
834        let Some((order, coefficient)) = found else {
835            return Err(format!(
836                "offset chart: S_{} vanishes to order {degree} across the boundary of \
837                 [{u0}, {u1}] x [{v0}, {v1}] at (u, v) = ({at_u}, {at_v}), so the surface does \
838                 not leave that boundary and has no regular parameter there",
839                if along_u { "u" } else { "v" }
840            ));
841        };
842        let speed = isoline_mean_speed(surface, along_u, anchor_u, anchor_v)?;
843        if !(speed > 0.0) {
844            return Ok(None);
845        }
846        Ok(Some(StationaryChart {
847            boundary,
848            outward: if boundary == hi { 1.0 } else { -1.0 },
849            order: order as i32,
850            coefficient,
851            speed,
852        }))
853    }
854}
855
856/// The body of the former `offset::stable_face_normal`, verbatim, with the
857/// `FaceRecord` replaced by the `same_sense` flag it read (audit §4.5: the
858/// `FaceRecord` requirement is a type mismatch, not a geometric one — it is why
859/// `thicken` fabricates a synthetic face with empty loops just to offset a bare
860/// surface).
861fn stable_normal(
862    surface: &NurbsSurface,
863    same_sense: bool,
864    u: f64,
865    v: f64,
866) -> Result<Vec3, String> {
867    let normal_at = |u, v| surface.normal(u, v).ok();
868    let mut normal = normal_at(u, v);
869    let ([u0, u1], [v0, v1]) = domains(surface)?;
870    if normal.is_none() {
871        let du = (u1 - u0) * 1e-5;
872        let dv = (v1 - v0) * 1e-5;
873        for (candidate_u, candidate_v) in [
874            ((u + du).clamp(u0, u1), v),
875            ((u - du).clamp(u0, u1), v),
876            (u, (v + dv).clamp(v0, v1)),
877            (u, (v - dv).clamp(v0, v1)),
878        ] {
879            normal = normal_at(candidate_u, candidate_v);
880            if normal.is_some() {
881                break;
882            }
883        }
884    }
885    // SINGULAR-ROW override: at a surface singularity where a whole
886    // parameter row collapses to one point (a cone apex), the at-point /
887    // nudged normal is the cross of a vanishing partial with noise — the
888    // AXIS direction instead of the ruling normal, which offsets the apex
889    // row straight down the axis and bends the fitted surface by exactly
890    // d·cos(half-angle). Detect the collapse by local point spread, walk
891    // DEEP inward for the true per-ruling limit, and replace the at-point
892    // value only when the two genuinely DISAGREE. A sphere/dome pole also
893    // reads as collapsed, but there the at-point normal (the axis) IS the
894    // limit — agreement keeps the exact baseline value.
895    //
896    // (`OffsetNormal::ExactAnalytic` answers this case in closed form for a
897    // recognised cone — the ruling's own azimuth, not a deep-inward probe —
898    // but no consumer rides that lane yet, so this stays the `offset_surface`
899    // behaviour it always was.)
900    let singular_here = {
901        let du = (u1 - u0) * 1e-4;
902        let dv = (v1 - v0) * 1e-4;
903        let here = surface.evaluate(u, v)?;
904        let along_u = surface
905            .evaluate((u + du).clamp(u0, u1), v)?
906            .sub(here)
907            .length()
908            .max(
909                surface
910                    .evaluate((u - du).clamp(u0, u1), v)?
911                    .sub(here)
912                    .length(),
913            );
914        let along_v = surface
915            .evaluate(u, (v + dv).clamp(v0, v1))?
916            .sub(here)
917            .length()
918            .max(
919                surface
920                    .evaluate(u, (v - dv).clamp(v0, v1))?
921                    .sub(here)
922                    .length(),
923            );
924        let scale = along_u.max(along_v);
925        scale > 0.0 && along_u.min(along_v) < scale * 1e-6
926    };
927    if singular_here {
928        let v_mid = (v0 + v1) * 0.5;
929        let u_mid = (u0 + u1) * 0.5;
930        let mut interior = None;
931        for fraction in [1e-3, 1e-2, 5e-2, 0.25] {
932            let candidate_v = v + (v_mid - v) * fraction;
933            let candidate_u = u + (u_mid - u) * fraction;
934            for (cu, cv) in [(u, candidate_v), (candidate_u, v), (candidate_u, candidate_v)] {
935                if let Some(candidate) = normal_at(cu, cv) {
936                    interior = Some(candidate);
937                    break;
938                }
939            }
940            if interior.is_some() {
941                break;
942            }
943        }
944        normal = match (normal, interior) {
945            (Some(at_point), Some(interior)) if at_point.dot(interior) > 1.0 - 1e-6 => {
946                Some(at_point)
947            }
948            (_, Some(interior)) => Some(interior),
949            (at_point, None) => at_point,
950        };
951    }
952    let normal =
953        normal.ok_or_else(|| "offset_surface: cannot determine surface normal".to_string())?;
954    Ok(orient(normal, same_sense))
955}
956
957// ---------------------------------------------------------------------------
958// The exact analytic lane
959// ---------------------------------------------------------------------------
960
961/// The exact unit normal of a recognised analytic carrier at `point`, in the
962/// carrier's own canonical orientation *calibrated to agree with `S_u × S_v`*.
963///
964/// Returns `Ok(None)` when there is no exact closed form for this carrier (the
965/// general `Revolution`, and every unrecognised free-form patch), so the caller
966/// can fall back.
967///
968/// **Sign.** The natural closed forms (a sphere's outward radial, a cone's
969/// meridian perpendicular) have no fixed sign relation to the patch's
970/// parametric normal — a `make_revolution` product's `S_u × S_v` points inward
971/// or outward depending on how the generatrix was oriented. So the exact
972/// direction is calibrated against the parametric normal once, at `(u, v)` if
973/// it is regular there and at the domain midpoint otherwise. If no regular
974/// probe exists anywhere tried, there is nothing to calibrate against and the
975/// lane declines.
976fn exact_analytic_normal(
977    surface: &NurbsSurface,
978    u: f64,
979    v: f64,
980    point: Vec3,
981) -> Result<Option<Vec3>, String> {
982    let Some(analytic) = surface.analytic() else {
983        return Ok(None);
984    };
985    let Some(direction) = exact_direction(surface, analytic, u, v, point)? else {
986        return Ok(None);
987    };
988    let ([u0, u1], [v0, v1]) = domains(surface)?;
989    // Calibration probes: here first (free agreement when the patch is regular
990    // at the query), then the domain midpoint, then two off-centre staggers
991    // that miss a seam or a pole row.
992    let probes = [
993        (u, v),
994        ((u0 + u1) * 0.5, (v0 + v1) * 0.5),
995        (u0 + (u1 - u0) * 0.37, v0 + (v1 - v0) * 0.41),
996        (u0 + (u1 - u0) * 0.63, v0 + (v1 - v0) * 0.59),
997    ];
998    for (pu, pv) in probes {
999        let Ok(parametric) = surface.normal(pu, pv) else {
1000            continue;
1001        };
1002        let probe_point = if (pu, pv) == (u, v) {
1003            point
1004        } else {
1005            surface.evaluate(pu, pv)?
1006        };
1007        let Some(probe_direction) = exact_direction(surface, analytic, pu, pv, probe_point)? else {
1008            continue;
1009        };
1010        let alignment = probe_direction.dot(parametric);
1011        // A probe whose two readings are near-orthogonal is not a calibration —
1012        // it is a degenerate row read as noise. Demand a decisive sign.
1013        if alignment.abs() < 0.5 {
1014            continue;
1015        }
1016        return Ok(Some(if alignment > 0.0 {
1017            direction
1018        } else {
1019            direction.scale(-1.0)
1020        }));
1021    }
1022    Ok(None)
1023}
1024
1025/// The closed-form normal direction (unit, canonical orientation, uncalibrated)
1026/// of one analytic carrier at a point known to lie on it.
1027fn exact_direction(
1028    surface: &NurbsSurface,
1029    analytic: &AnalyticSurface,
1030    u: f64,
1031    v: f64,
1032    point: Vec3,
1033) -> Result<Option<Vec3>, String> {
1034    Ok(match analytic {
1035        AnalyticSurface::Plane { u_dir, v_dir, .. } => u_dir.cross(*v_dir).normalized().ok(),
1036        AnalyticSurface::Sphere { frame, .. } => {
1037            // Exact AT the poles, which is precisely where `S_u × S_v`
1038            // vanishes and the Greville fit that `face_offset_sphere.rs`
1039            // rejected went wrong.
1040            point.sub(frame.origin).normalized().ok()
1041        }
1042        AnalyticSurface::RuledRevolution {
1043            frame,
1044            rho0,
1045            rho1,
1046            height,
1047        } => {
1048            let radial = match radial_direction(surface, frame.origin, frame.axis, u, v, point)? {
1049                Some(radial) => radial,
1050                None => return Ok(None),
1051            };
1052            // Meridian generatrix runs (rho0, 0) → (rho1, height) in
1053            // (radial, axial); the in-meridian perpendicular is
1054            // (height, −(rho1 − rho0)) over its length.
1055            let dr = rho1 - rho0;
1056            let length = (dr * dr + height * height).sqrt();
1057            if length <= 0.0 {
1058                return Ok(None);
1059            }
1060            radial
1061                .scale(*height / length)
1062                .sub(frame.axis.scale(dr / length))
1063                .normalized()
1064                .ok()
1065        }
1066        AnalyticSurface::Torus {
1067            frame,
1068            major_radius,
1069            minor_radius,
1070        } => {
1071            let radial = match radial_direction(surface, frame.origin, frame.axis, u, v, point)? {
1072                Some(radial) => radial,
1073                None => return Ok(None),
1074            };
1075            if *minor_radius <= 0.0 {
1076                return Ok(None);
1077            }
1078            let tube_centre = frame.origin.add(radial.scale(*major_radius));
1079            point.sub(tube_centre).normalized().ok()
1080        }
1081        // A general revolution's exact normal needs the generatrix tangent at
1082        // the meridian station, which is a 1D projection — not closed form, and
1083        // no cheaper than `S_u × S_v`. Decline rather than pretend.
1084        AnalyticSurface::Revolution { .. } => None,
1085    })
1086}
1087
1088/// Unit radial direction of `point` about the axis.
1089///
1090/// On the axis itself (a cone apex, or a sphere/torus degeneracy) the point
1091/// carries no azimuth — but the *parameter* still does, because `u` names the
1092/// ruling. Recover it from the opposite end of the same `u` iso-line, which is
1093/// the exact per-ruling limit the `FaceStable` recovery only approximates by
1094/// walking inward.
1095fn radial_direction(
1096    surface: &NurbsSurface,
1097    origin: Vec3,
1098    axis: Vec3,
1099    u: f64,
1100    v: f64,
1101    point: Vec3,
1102) -> Result<Option<Vec3>, String> {
1103    let radial_of = |point: Vec3| {
1104        let relative = point.sub(origin);
1105        relative.sub(axis.scale(relative.dot(axis)))
1106    };
1107    let here = radial_of(point);
1108    let [v0, v1] = surface.domain_v()?;
1109    let scale = point.sub(origin).length().max(1.0);
1110    if here.length() > 1e-12 * scale {
1111        return Ok(here.normalized().ok());
1112    }
1113    let far = if (v - v0).abs() >= (v1 - v).abs() {
1114        v0
1115    } else {
1116        v1
1117    };
1118    let candidate = radial_of(surface.evaluate(u, far)?);
1119    if candidate.length() <= 1e-12 * scale {
1120        return Ok(None);
1121    }
1122    Ok(candidate.normalized().ok())
1123}
1124
1125// ---------------------------------------------------------------------------
1126// Migration diagnostic
1127// ---------------------------------------------------------------------------
1128
1129/// Per-site agreement between the normal lanes, aggregated over one thread.
1130///
1131/// Enabled by `BREP_OFFSET_DIAG`; the value is a sampling stride (`1` = every
1132/// call). Printing per call is useless here — one blend march makes hundreds of
1133/// thousands of evaluations — so the comparison is accumulated and dumped when
1134/// the thread ends, which for `cargo test` is once per test.
1135///
1136/// A site with `calls` but zero disagreement is evidence the corpus does not
1137/// *exercise* the difference, not proof the lanes are equivalent; a site that
1138/// never appears was never reached at all. Both readings matter, so the counts
1139/// are reported alongside the magnitudes.
1140#[derive(Default, Clone, Copy)]
1141struct LaneStats {
1142    compared: u64,
1143    /// Calls where this lane errored but the selected lane did not.
1144    lane_errors: u64,
1145    /// Calls where this lane succeeded and the selected lane did not.
1146    lane_rescues: u64,
1147    /// Worst CHORD distance between this lane's unit normal and the selected
1148    /// lane's, taken as an unsigned DIRECTION: `min(|a − b|, |a + b|)`.
1149    ///
1150    /// Chord, not `acos`: two bitwise identical unit vectors have a dot product
1151    /// one ULP below 1.0 as often as not (three rounded products summed), and
1152    /// `acos` turns that ULP into a flat 1.49e-8 rad floor that hides every
1153    /// real signal beneath it.  The chord is `2·sin(θ/2)`, so for a small angle
1154    /// it IS the angle in radians, and it is exactly 0 for identical vectors.
1155    ///
1156    /// Unsigned, because `Raw` carries no orientation while the `Face` lanes
1157    /// apply `same_sense`: on a reversed face they are exact opposites, and
1158    /// that is the CONVENTION difference (§4.2), not a geometry difference.
1159    /// The flips are counted separately so neither reading is lost.
1160    worst_chord: f64,
1161    /// Calls where this lane's normal pointed OPPOSITE the selected lane's.
1162    flipped: u64,
1163    worst_u: f64,
1164    worst_v: f64,
1165}
1166
1167#[derive(Default, Clone, Copy)]
1168struct SiteStats {
1169    calls: u64,
1170    /// Calls where the lane the caller SELECTED could not produce a normal.
1171    errors: u64,
1172    convention: Option<OffsetNormal>,
1173    lanes: [LaneStats; 4],
1174}
1175
1176const LANE_NAMES: [&str; 4] = ["raw", "face", "stable", "exact"];
1177
1178fn lane_of(index: usize, same_sense: bool) -> OffsetNormal {
1179    match index {
1180        0 => OffsetNormal::Raw,
1181        1 => OffsetNormal::Face { same_sense },
1182        2 => OffsetNormal::FaceStable { same_sense },
1183        _ => OffsetNormal::ExactAnalytic { same_sense },
1184    }
1185}
1186
1187struct DiagnosticSink {
1188    sites: std::collections::BTreeMap<&'static str, SiteStats>,
1189}
1190
1191impl Drop for DiagnosticSink {
1192    fn drop(&mut self) {
1193        use std::fmt::Write as _;
1194        for (site, stats) in &self.sites {
1195            // Build the WHOLE line first and emit it with ONE `eprintln!`.
1196            // Threads exit concurrently under `cargo test`, and a report
1197            // assembled from a run of `eprint!`s interleaves mid-line with
1198            // another thread's — the first version of this lost ~60% of its
1199            // reports to exactly that, which looked like missing sites rather
1200            // than shredded ones.
1201            let mut line = format!(
1202                "offset-diag site={site} convention={} calls={} errors={}",
1203                stats
1204                    .convention
1205                    .map(|convention| format!("{convention:?}"))
1206                    .unwrap_or_else(|| "?".to_string()),
1207                stats.calls,
1208                stats.errors
1209            );
1210            for (index, lane) in stats.lanes.iter().enumerate() {
1211                if lane.compared == 0 && lane.lane_errors == 0 && lane.lane_rescues == 0 {
1212                    continue;
1213                }
1214                let _ = write!(
1215                    line,
1216                    " | {}: n={} chord={:.3e} at=({:.6},{:.6}) flip={} lane_err={} rescue={}",
1217                    LANE_NAMES[index],
1218                    lane.compared,
1219                    lane.worst_chord,
1220                    lane.worst_u,
1221                    lane.worst_v,
1222                    lane.flipped,
1223                    lane.lane_errors,
1224                    lane.lane_rescues
1225                );
1226            }
1227            eprintln!("{line}");
1228        }
1229    }
1230}
1231
1232fn diagnostic_stride() -> u64 {
1233    static STRIDE: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
1234    *STRIDE.get_or_init(|| match std::env::var("BREP_OFFSET_DIAG") {
1235        Ok(value) => value.trim().parse::<u64>().unwrap_or(1).max(1),
1236        Err(_) => 0,
1237    })
1238}
1239
1240thread_local! {
1241    static DIAGNOSTIC: std::cell::RefCell<DiagnosticSink> = std::cell::RefCell::new(DiagnosticSink {
1242        sites: std::collections::BTreeMap::new(),
1243    });
1244}
1245
1246/// Compare every lane against the one the caller selected, and accumulate.
1247///
1248/// Off unless `BREP_OFFSET_DIAG` is set, and then the only cost on the hot path
1249/// is one `OnceLock` read and a modulo.
1250fn offset_normal_diagnostic(
1251    site: &'static str,
1252    surface: &NurbsSurface,
1253    convention: OffsetNormal,
1254    u: f64,
1255    v: f64,
1256    selected: &Result<Vec3, String>,
1257) {
1258    let stride = diagnostic_stride();
1259    if stride == 0 {
1260        return;
1261    }
1262    let sampled = DIAGNOSTIC.with(|sink| {
1263        let mut sink = sink.borrow_mut();
1264        let stats = sink.sites.entry(site).or_default();
1265        stats.convention = Some(convention);
1266        stats.calls += 1;
1267        if selected.is_err() {
1268            stats.errors += 1;
1269        }
1270        stats.calls % stride == 0
1271    });
1272    if !sampled {
1273        return;
1274    }
1275    let evaluator = OffsetEvaluator::new(site, surface, convention);
1276    let same_sense = convention.same_sense();
1277    let mut readings = [(0usize, f64::NAN, false, false); 4];
1278    for (index, reading) in readings.iter_mut().enumerate() {
1279        let lane = lane_of(index, same_sense);
1280        let value = evaluator.normal_with(lane, u, v);
1281        *reading = match (selected, &value) {
1282            (Ok(chosen), Ok(other)) => {
1283                let aligned = chosen.sub(*other).length();
1284                let opposed = chosen.add(*other).length();
1285                (index, aligned.min(opposed), true, opposed < aligned)
1286            }
1287            (Ok(_), Err(_)) => (index, f64::NAN, false, false),
1288            (Err(_), Ok(_)) => (index, f64::NAN, true, false),
1289            (Err(_), Err(_)) => (index, f64::NAN, false, false),
1290        };
1291    }
1292    DIAGNOSTIC.with(|sink| {
1293        let mut sink = sink.borrow_mut();
1294        let stats = sink.sites.entry(site).or_default();
1295        for (index, chord, lane_ok, flipped) in readings {
1296            let lane = &mut stats.lanes[index];
1297            if chord.is_finite() {
1298                lane.compared += 1;
1299                if flipped {
1300                    lane.flipped += 1;
1301                }
1302                if chord > lane.worst_chord {
1303                    lane.worst_chord = chord;
1304                    lane.worst_u = u;
1305                    lane.worst_v = v;
1306                }
1307            } else if selected.is_err() && lane_ok {
1308                lane.lane_rescues += 1;
1309            } else if selected.is_ok() && !lane_ok {
1310                lane.lane_errors += 1;
1311            }
1312        }
1313    });
1314}
1315