Skip to main content

brep_kernel/offset/
offset.rs

1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use crate::fit::{solve_dense, solve_dense_shared};
3use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, VertexRecord};
4use crate::{
5    interpolate_curve, measure_edge_against_pcurve_image,
6    measure_surface_fit_against_pointwise_offset, offset_construction_band, solid_model_scale,
7    vertex_endpoint_gap, vertex_tolerance_from_edges, KnotVector, MeasuredTolerance, NurbsCurve,
8    NurbsSurface, OffsetEvaluator, OffsetNormal, Vec2, Vec3, Vec4,
9};
10use rustc_hash::FxHashMap as HashMap;
11use serde::Serialize;
12
13fn domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
14    Ok((
15        KnotVector::new(surface.knots_u.clone(), surface.degree_u).or_refuse(KernelStage::Refine, "new")?.domain(),
16        KnotVector::new(surface.knots_v.clone(), surface.degree_v).or_refuse(KernelStage::Refine, "new")?.domain(),
17    ))
18}
19
20/// The normal `offset_surface` offsets along: the face's oriented normal with
21/// the singular-row recovery, i.e. the shared evaluator's
22/// [`OffsetNormal::FaceStable`] lane, whose body this function used to be.
23fn stable_face_normal(face: &FaceRecord, u: f64, v: f64) -> Result<Vec3, KernelRefusal> {
24    OffsetEvaluator::new(
25        "offset_surface",
26        &face.surface,
27        OffsetNormal::FaceStable {
28            same_sense: face.same_sense,
29        },
30    )
31    .normal(u, v)
32    .or_refuse(KernelStage::Refine, "normal")
33}
34
35fn greville_parameters(knots: &KnotVector) -> Vec<f64> {
36    let mut parameters = (0..knots.control_point_count())
37        .map(|index| {
38            knots.knots[index + 1..=index + knots.degree]
39                .iter()
40                .sum::<f64>()
41                / knots.degree as f64
42        })
43        .collect::<Vec<_>>();
44    let domain = knots.domain();
45    parameters[0] = domain[0];
46    *parameters.last_mut().unwrap() = domain[1];
47    parameters
48}
49
50/// Rational collocation matrix: rows are the rational basis functions
51/// R_i(t) = N_i(t)·w_i / Σ_k N_k(t)·w_k evaluated at each parameter. With
52/// uniform weights this reduces to the ordinary B-spline collocation matrix.
53fn collocation_matrix(knots: &KnotVector, parameters: &[f64], weights: &[f64]) -> Vec<Vec<f64>> {
54    parameters
55        .iter()
56        .map(|parameter| {
57            let mut row = vec![0.0; knots.control_point_count()];
58            let span = knots.find_span(*parameter);
59            for (offset, value) in knots
60                .basis_functions(span, *parameter)
61                .into_iter()
62                .enumerate()
63            {
64                let index = span - knots.degree + offset;
65                row[index] = value * weights[index];
66            }
67            let denominator: f64 = row.iter().sum();
68            if denominator.abs() > 0.0 {
69                for value in &mut row {
70                    *value /= denominator;
71                }
72            }
73            row
74        })
75        .collect()
76}
77
78/// Split the weight grid into per-direction factors when it is separable
79/// (w_ij = a_i·b_j), which covers every tensor surface built from rational
80/// profile/rail curves (cylinders, cones, spheres, tori, revolves).
81fn separable_weights(weights: &[Vec<f64>]) -> Option<(Vec<f64>, Vec<f64>)> {
82    let first_row = weights.first()?;
83    let anchor = *first_row.first()?;
84    if anchor.abs() <= 1e-12 {
85        return None;
86    }
87    let a: Vec<f64> = weights.iter().map(|row| row[0]).collect();
88    let b: Vec<f64> = first_row.iter().map(|w| w / anchor).collect();
89    for (i, row) in weights.iter().enumerate() {
90        for (j, &w) in row.iter().enumerate() {
91            if (w - a[i] * b[j]).abs() > 1e-10 * (1.0 + w.abs()) {
92                return None;
93            }
94        }
95    }
96    Some((a, b))
97}
98
99/// Interpolate the sample grid in the SOURCE surface's rational basis (same
100/// knots and weights). When the true offset is representable in that basis —
101/// planes, cylinders, cones, spheres, tori — collocation at the Greville grid
102/// recovers it EXACTLY, so offset carriers stay real analytic surfaces
103/// instead of non-rational approximations with span-scale wobble.
104fn interpolate_tensor(
105    knot_u: &KnotVector,
106    knot_v: &KnotVector,
107    parameters_u: &[f64],
108    parameters_v: &[f64],
109    samples: &[Vec<Vec3>],
110    weights: &[Vec<f64>],
111) -> Result<Vec<Vec<Vec4>>, KernelRefusal> {
112    let count_u = parameters_u.len();
113    let count_v = parameters_v.len();
114    if let Some((weights_u, weights_v)) = separable_weights(weights) {
115        let matrix_u = collocation_matrix(knot_u, parameters_u, &weights_u);
116        let matrix_v = collocation_matrix(knot_v, parameters_v, &weights_v);
117        // Each column's three coordinates share one elimination
118        // (`solve_dense_shared`, the bits of three `solve_dense` calls on
119        // clones). The collocation matrix is banded, and the dense solve spent
120        // `n^3` on products with zero: ABC 00027566 offsets 82 extruded cubics
121        // whose fit refines to 1239 x 2, 13.7 s a face, and the STEP import
122        // never finished (2026-11-10).
123        let mut intermediate = vec![vec![Vec3::default(); count_v]; count_u];
124        for column in 0..count_v {
125            let side = |axis: fn(Vec3) -> f64| samples.iter().map(|row| axis(row[column])).collect::<Vec<_>>();
126            let [x, y, z] = solve_dense_shared(matrix_u.clone(), [side(|point| point.x), side(|point| point.y), side(|point| point.z)])
127                .or_refuse(KernelStage::Refine, "solve_axis")?;
128            for row in 0..count_u {
129                intermediate[row][column] = Vec3::new(x[row], y[row], z[row]);
130            }
131        }
132        let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
133        for row in 0..count_u {
134            let side = |axis: fn(Vec3) -> f64| intermediate[row].iter().copied().map(axis).collect::<Vec<_>>();
135            let [x, y, z] = solve_dense_shared(matrix_v.clone(), [side(|point| point.x), side(|point| point.y), side(|point| point.z)])
136                .or_refuse(KernelStage::Refine, "solve_axis")?;
137            for column in 0..count_v {
138                controls[row][column] = Vec4::from_point(
139                    Vec3::new(x[column], y[column], z[column]),
140                    weights[row][column],
141                );
142            }
143        }
144        return Ok(controls);
145    }
146
147    // Non-separable weights: solve the full tensor collocation system with
148    // the exact 2D rational basis. Nets are small in practice.
149    let unknowns = count_u * count_v;
150    let mut matrix = vec![vec![0.0; unknowns]; unknowns];
151    for (k, &u) in parameters_u.iter().enumerate() {
152        let span_u = knot_u.find_span(u);
153        let basis_u = knot_u.basis_functions(span_u, u);
154        for (l, &v) in parameters_v.iter().enumerate() {
155            let span_v = knot_v.find_span(v);
156            let basis_v = knot_v.basis_functions(span_v, v);
157            let row = &mut matrix[k * count_v + l];
158            let mut denominator = 0.0;
159            for (du, value_u) in basis_u.iter().enumerate() {
160                let i = span_u - knot_u.degree + du;
161                for (dv, value_v) in basis_v.iter().enumerate() {
162                    let j = span_v - knot_v.degree + dv;
163                    let entry = value_u * value_v * weights[i][j];
164                    row[i * count_v + j] = entry;
165                    denominator += entry;
166                }
167            }
168            if denominator.abs() > 0.0 {
169                for value in row.iter_mut() {
170                    *value /= denominator;
171                }
172            }
173        }
174    }
175    let solve_axis = |axis: fn(Vec3) -> f64| {
176        solve_dense(
177            matrix.clone(),
178            samples
179                .iter()
180                .flat_map(|row| row.iter().copied().map(axis))
181                .collect(),
182        )
183    };
184    let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
185    let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
186    let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
187    let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
188    for row in 0..count_u {
189        for column in 0..count_v {
190            let index = row * count_v + column;
191            controls[row][column] = Vec4::from_point(
192                Vec3::new(x[index], y[index], z[index]),
193                weights[row][column],
194            );
195        }
196    }
197    Ok(controls)
198}
199
200/// Which branch of [`offset_surface`] built a carrier — and, with it, whether
201/// comparing that carrier against the pointwise offset at the same `(u, v)` is
202/// even the right question.
203///
204/// Recorded rather than inferred, in the pattern `offset/reintersect.rs`
205/// established for its own two lanes: two of this function's three branches
206/// deliberately move the result off the pointwise offset, and a measurement that
207/// did not know which branch ran would report a designed divergence as a fit
208/// error.
209#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
210pub enum OffsetSurfaceLane {
211    /// A rigid control-net shift of an affine (plane-like) carrier. The normal
212    /// is constant, so the shifted net IS the pointwise offset — exact by
213    /// construction, with no fit to measure.
214    Affine,
215    /// A CLOSED-FORM offset built from the source's recognized analytic shape
216    /// rather than sampled: a sphere's concentric mate, whose net is the
217    /// source's scaled about the centre. Exact like `Affine`, but derived from
218    /// geometry the recognizer proved rather than from a constant normal, so
219    /// `offset_surface_measured_sided` measures it instead of declaring it.
220    Analytic,
221    /// Greville collocation of the pointwise offset. `S_fit(u, v)` is meant to
222    /// BE `offset(u, v)`, and the distance between them is the fit error this
223    /// slice measures.
224    #[default]
225    Fit,
226    /// The result was deliberately moved off the pointwise offset: the planar /
227    /// ruled EXTENSION (which grows the carrier past its source rim, so the same
228    /// `(u, v)` names a different point) or the apex-cone PINCH RETRIM (which
229    /// pulls the crossed sample row back to the offset cone's own apex). Both
230    /// are correct and intended; neither is comparable pointwise.
231    Reparameterised,
232}
233
234/// A fitted offset carrier together with what its construction measured about
235/// itself.
236#[derive(Clone, Debug)]
237pub struct MeasuredOffsetSurface {
238    pub surface: NurbsSurface,
239    pub lane: OffsetSurfaceLane,
240    /// `max ‖S_fit(u, v) − offset(u, v)‖` over
241    /// [`crate::measure_surface_fit_against_pointwise_offset`]'s grid, against
242    /// the band it was judged with. When the RULED EXTENSION fired, the offset
243    /// compared against is the offset of the source EXTENDED ALONG ITS RULINGS —
244    /// the surface the carrier is the pointwise offset of — not of `face.surface`.
245    ///
246    /// `None` for [`OffsetSurfaceLane::Reparameterised`] — see that variant.
247    pub fit: Option<MeasuredTolerance>,
248}
249
250/// How far an offset carrier's TRIM grows past the source face at each of
251/// its four parametric sides, in world units.
252///
253/// A carrier is trimmed by the parametric IMAGE of the source loops, so the
254/// growth is realised by reshaping the carrier surface's net while the cloned
255/// pcurves stay put: an affine plane's 2x2 net slides along the plane, a ruled
256/// (linear-v) net stretches each sampled ruling. Both are solved so that the
257/// trim's own uv bounding box moves by exactly these amounts — a trim that
258/// occupies a sub-range of its surface's domain (every booleaned or
259/// blend-trimmed face) grows by the same distance as one that spans it. The
260/// legacy `planar_extension: f64` entry points are the uniform case.
261#[derive(Clone, Copy, Debug, PartialEq)]
262pub struct CarrierExtension {
263    pub u_min: f64,
264    pub u_max: f64,
265    pub v_min: f64,
266    pub v_max: f64,
267}
268
269impl CarrierExtension {
270    pub const NONE: CarrierExtension = CarrierExtension {
271        u_min: 0.0,
272        u_max: 0.0,
273        v_min: 0.0,
274        v_max: 0.0,
275    };
276
277    pub fn uniform(amount: f64) -> CarrierExtension {
278        CarrierExtension {
279            u_min: amount,
280            u_max: amount,
281            v_min: amount,
282            v_max: amount,
283        }
284    }
285
286    pub fn is_active(&self) -> bool {
287        self.u_min > 0.0 || self.u_max > 0.0 || self.v_min > 0.0 || self.v_max > 0.0
288    }
289
290    fn extends_v(&self) -> bool {
291        self.v_min > 0.0 || self.v_max > 0.0
292    }
293}
294
295/// Lane R2 row 9 (2026-10-19): `BREP_OFFSET_EXACT_TRIM_BOX=1`/`on` closes
296/// [`trim_domain_fractions`]' sampled box on each pcurve's true extremes
297/// ([`refine_trim_extreme`]; the default since its census at 9183d81d4);
298/// `=0`/`off` keeps the 25-sample box; `set_exact_trim_box_for_test` forces
299/// it on a test's own thread.
300///
301/// The slide is solved for that box, so a box short of the trim slides the
302/// net unevenly and the cloned pcurves leave their edges.
303const EXACT_TRIM_BOX_DEFAULT: bool = true;
304
305fn exact_trim_box_on() -> bool {
306    switch!(live "BREP_OFFSET_EXACT_TRIM_BOX", EXACT_TRIM_BOX_DEFAULT)
307}
308
309
310/// Golden-section steps [`refine_trim_extreme`] takes: the bracket shrinks to
311/// 0.618^64 ≈ 4e-14 of two sample spacings, past where the extreme's value
312/// can still move (it is quadratic in the parameter there).
313const TRIM_EXTREME_STEPS: usize = 64;
314
315/// The extreme of `sign · uv[axis]` (`axis` 0 is u, 1 is v) over one pcurve,
316/// returned as the coordinate itself. Bracketed by the pcurve's best sample
317/// and that sample's two neighbours, and closed by a golden-section search of
318/// a fixed [`TRIM_EXTREME_STEPS`] steps; never worse than the best sample.
319/// On an arc the bracketed piece is unimodal, so this is its true extreme.
320/// A line's extremes are its ends, which are samples, so the caller does not
321/// search one (an interior read of a straight trim can land an ulp past its
322/// end).
323fn refine_trim_extreme(
324    pcurve: &NurbsCurve,
325    samples: &[(f64, Vec3)],
326    axis: usize,
327    sign: f64,
328) -> Result<f64, KernelRefusal> {
329    let read = |uv: Vec3| sign * if axis == 0 { uv.x } else { uv.y };
330    let at = |parameter: f64| -> Result<f64, KernelRefusal> {
331        Ok(read(pcurve.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?))
332    };
333    let best = (0..samples.len())
334        .max_by(|&a, &b| read(samples[a].1).total_cmp(&read(samples[b].1)))
335        .expect("a pcurve has samples");
336    let mut found = read(samples[best].1);
337    let (mut a, mut b) = (samples[best.saturating_sub(1)].0, samples[(best + 1).min(samples.len() - 1)].0);
338    const GOLDEN: f64 = 0.618_033_988_749_894_9;
339    let mut c = b - GOLDEN * (b - a);
340    let mut d = a + GOLDEN * (b - a);
341    let (mut fc, mut fd) = (at(c)?, at(d)?);
342    for _ in 0..TRIM_EXTREME_STEPS {
343        found = found.max(fc).max(fd);
344        if fc >= fd {
345            (b, d, fd) = (d, c, fc);
346            c = b - GOLDEN * (b - a);
347            fc = at(c)?;
348        } else {
349            (a, c, fc) = (c, d, fd);
350            d = a + GOLDEN * (b - a);
351            fd = at(d)?;
352        }
353    }
354    Ok(sign * found.max(fc).max(fd))
355}
356
357/// The trim's uv bounding box as FRACTIONS of the surface domain:
358/// `([ta_u, tb_u], [ta_v, tb_v])`, each in `[0, 1]`. Sampled over every
359/// coedge pcurve of every loop, and closed on the pcurves' true extremes
360/// under `BREP_OFFSET_EXACT_TRIM_BOX`.
361fn trim_domain_fractions(
362    face: &FaceRecord,
363    [u0, u1]: [f64; 2],
364    [v0, v1]: [f64; 2],
365) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
366    let mut low = Vec2 {
367        x: f64::INFINITY,
368        y: f64::INFINITY,
369    };
370    let mut high = Vec2 {
371        x: f64::NEG_INFINITY,
372        y: f64::NEG_INFINITY,
373    };
374    for coedge in face
375        .loops
376        .iter()
377        .flat_map(|loop_record| &loop_record.coedges)
378    {
379        let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
380        let mut samples = Vec::with_capacity(25);
381        for sample in 0..=24 {
382            let parameter = p0 + (p1 - p0) * sample as f64 / 24.0;
383            let uv = coedge
384                .pcurve
385                .evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?;
386            low.x = low.x.min(uv.x);
387            low.y = low.y.min(uv.y);
388            high.x = high.x.max(uv.x);
389            high.y = high.y.max(uv.y);
390            samples.push((parameter, uv));
391        }
392        if coedge.pcurve.degree > 1 && exact_trim_box_on() {
393            low.x = low.x.min(refine_trim_extreme(&coedge.pcurve, &samples, 0, -1.0)?);
394            high.x = high.x.max(refine_trim_extreme(&coedge.pcurve, &samples, 0, 1.0)?);
395            low.y = low.y.min(refine_trim_extreme(&coedge.pcurve, &samples, 1, -1.0)?);
396            high.y = high.y.max(refine_trim_extreme(&coedge.pcurve, &samples, 1, 1.0)?);
397        }
398    }
399    let fraction = |value: f64, start: f64, end: f64| {
400        let span = end - start;
401        if span.abs() <= f64::EPSILON || !value.is_finite() {
402            return None;
403        }
404        Some(((value - start) / span).clamp(0.0, 1.0))
405    };
406    let u = match (fraction(low.x, u0, u1), fraction(high.x, u0, u1)) {
407        (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
408        _ => [0.0, 1.0],
409    };
410    let v = match (fraction(low.y, v0, v1), fraction(high.y, v0, v1)) {
411        (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
412        _ => [0.0, 1.0],
413    };
414    Ok((u, v))
415}
416
417/// How far the surface's domain ENDS must move (`(back, forward)`: the start
418/// end backwards, the far end forwards, world units) so that a trim occupying
419/// the domain fractions `[ta, tb]` grows by exactly `grow_min` at its start and
420/// `grow_max` at its end when its pcurves are left untouched. A point at
421/// fraction `t` of a linearly re-mapped domain moves by
422/// `-(1 - t)·back + t·forward`; solving that at `ta` and `tb` gives the pair
423/// below. It reduces to `(grow_min, grow_max)` for a full-domain trim, and
424/// never shrinks either end (both results are non-negative for non-negative
425/// inputs).
426fn domain_end_moves(grow_min: f64, grow_max: f64, [ta, tb]: [f64; 2]) -> (f64, f64) {
427    let span = tb - ta;
428    if span <= 1e-6 {
429        return (grow_min, grow_max);
430    }
431    let rate = (grow_min + grow_max) / span;
432    let back = grow_min + ta * rate;
433    let forward = (1.0 - ta) * rate - grow_min;
434    (back.max(0.0), forward.max(0.0))
435}
436
437/// Construct the same fitted offset carrier surface as the reference shell
438/// implementation. Positive distance follows its convention and moves
439/// opposite the face's outward normal.
440pub fn offset_surface(
441    face: &FaceRecord,
442    distance: f64,
443    planar_extension: f64,
444) -> Result<NurbsSurface, KernelRefusal> {
445    offset_surface_with_lane(face, distance, &CarrierExtension::uniform(planar_extension))
446        .map(|(surface, _)| surface)
447}
448
449thread_local! {
450    static IMPORT_OFFSET_PRECISION: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
451}
452
453/// Import needs the pointwise offset carrier, without the construction-fit
454/// floor becoming an accuracy limit for downstream surface integrals. Elevate
455/// the rational basis exactly before collocation: increasing approximation
456/// order resolves the normal field without a quadratic net's excessive span
457/// growth. The ordinary construction budget and acceptance gate still apply.
458pub(crate) fn offset_surface_for_import(face: &FaceRecord, distance: f64) -> Result<NurbsSurface, KernelRefusal> {
459    if face.surface.analytic().is_some() || face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
460        return offset_surface(face, distance, 0.0);
461    }
462    let source = &face.surface;
463    let mut rows = Vec::new();
464    let mut knots_v = source.knots_v.clone();
465    for row in &source.control_points {
466        let curve = NurbsCurve::new(source.degree_v, source.knots_v.clone(), row.clone()).or_refuse(KernelStage::Refine, "new")?;
467        let raised = crate::coons::compat::elevate_degree(&curve, source.degree_v.max(5)).or_refuse(KernelStage::Refine, "elevate_degree")?;
468        knots_v = raised.knots.clone();
469        rows.push(raised.control_points);
470    }
471    let mut columns = Vec::new();
472    let mut knots_u = source.knots_u.clone();
473    for column in 0..rows[0].len() {
474        let curve = NurbsCurve::new(source.degree_u, source.knots_u.clone(), rows.iter().map(|row| row[column]).collect()).or_refuse(KernelStage::Refine, "new")?;
475        let raised = crate::coons::compat::elevate_degree(&curve, source.degree_u.max(5)).or_refuse(KernelStage::Refine, "elevate_degree")?;
476        knots_u = raised.knots.clone();
477        columns.push(raised.control_points);
478    }
479    let controls = (0..columns[0].len()).map(|row| columns.iter().map(|column| column[row]).collect()).collect();
480    let mut elevated = face.clone();
481    elevated.surface = NurbsSurface::new(source.degree_u.max(5), source.degree_v.max(5), knots_u, knots_v, controls).or_refuse(KernelStage::Refine, "new")?;
482    // The normal differentiates the carrier once: a C^k source generally
483    // has a C^(k-1) offset field. Exact knot insertion leaves the source
484    // unchanged but lets collocation represent that lower continuity.
485    let interior = |knots: &[f64], degree: usize| {
486        let low = knots[degree];
487        let high = knots[knots.len() - degree - 1];
488        let mut values: Vec<f64> = knots.iter().copied().filter(|k| *k > low && *k < high).collect();
489        values.dedup();
490        values
491    };
492    elevated.surface = insert_offset_fit_knots(&elevated.surface,
493        &interior(&source.knots_u, source.degree_u),
494        &interior(&source.knots_v, source.degree_v))?;
495    struct Restore(bool);
496    impl Drop for Restore {
497        fn drop(&mut self) { IMPORT_OFFSET_PRECISION.with(|state| state.set(self.0)); }
498    }
499    let _restore = Restore(IMPORT_OFFSET_PRECISION.with(|state| state.replace(true)));
500    offset_surface(&elevated, distance, 0.0)
501}
502
503/// [`offset_surface`], plus the deviation MEASURED between the carrier it built
504/// and the pointwise offset that carrier approximates.
505///
506/// The surface is bit-identical to [`offset_surface`]'s — this calls the same
507/// body and adds a read-only pass afterwards. Nothing here can change the
508/// carrier: measuring the deviation AFTER the carrier is built records what
509/// happened; the size-derived band stays the floor and the bar it is judged
510/// against. A measurement never widens a band.
511pub fn offset_surface_measured(
512    face: &FaceRecord,
513    distance: f64,
514    planar_extension: f64,
515    band: f64,
516) -> Result<MeasuredOffsetSurface, KernelRefusal> {
517    offset_surface_measured_sided(
518        face,
519        distance,
520        &CarrierExtension::uniform(planar_extension),
521        band,
522    )
523}
524
525/// [`offset_surface_measured`] with a per-side [`CarrierExtension`].
526pub fn offset_surface_measured_sided(
527    face: &FaceRecord,
528    distance: f64,
529    extension: &CarrierExtension,
530    band: f64,
531) -> Result<MeasuredOffsetSurface, KernelRefusal> {
532    offset_surface_measured_sided_mode(face, distance, extension, band,
533        std::env::var("BREP_OFFSET_SPHERE_READING").map_or(true, |v| v != "0" && v != "off"))
534}
535
536fn offset_surface_measured_sided_mode(
537    face: &FaceRecord, distance: f64, extension: &CarrierExtension, band: f64, reuse: bool,
538) -> Result<MeasuredOffsetSurface, KernelRefusal> {
539    let mut sphere_reading = None;
540    let (surface, lane, extended) = offset_surface_constructed(face, distance, extension, &mut sphere_reading)?;
541    let fit = match lane {
542        OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
543        // `Analytic` is exact by construction and already verified against the
544        // pointwise offset before it was returned — but it is measured here
545        // like a fit anyway, so the record carries the number instead of the
546        // claim. It reads ~1e-13 where `Fit` reads ~1e-6.
547        OffsetSurfaceLane::Analytic if reuse && sphere_reading.is_some() => {
548            // The construction already read this exact source/result pair on
549            // the same grid. Only the report band differs; it is not a grid
550            // input. Retain the measured deviation, never substitute zero.
551            if crate::profile::profile_enabled() {
552                profile_line!("offset.sphere_reading.profile reused=1");
553            }
554            Some(MeasuredTolerance::new(sphere_reading.unwrap(), band))
555        }
556        OffsetSurfaceLane::Analytic | OffsetSurfaceLane::Fit => {
557            Some(measure_surface_fit_against_pointwise_offset(
558                extended.as_ref().unwrap_or(&face.surface),
559                face.same_sense,
560                &surface,
561                distance,
562                band,
563            )?)
564        }
565        OffsetSurfaceLane::Reparameterised => None,
566    };
567    Ok(MeasuredOffsetSurface { surface, lane, fit })
568}
569
570/// The EXACT concentric offset of a sphere, or `None` for every other carrier.
571///
572/// A sphere's normal is radial, so its pointwise offset at distance `d` is the
573/// concentric sphere of radius `R' = R ± d`. A NURBS map is affine in its
574/// control points — `S = Σ N_i w_i P_i / Σ N_i w_i` — so scaling every control
575/// point about the centre by `R'/R` scales the whole image by that factor about
576/// that point, and the result IS that sphere to rounding. Knots, degrees and
577/// weights are untouched, so the same `(u, v)` still names the corresponding
578/// point: the trims `offset_face_carrier_impl` clones onto this carrier
579/// verbatim land exactly where they did on the source.
580///
581/// The Greville fit below reaches the same surface only to its collocation
582/// error. On `offset-shell-box-bore-dished-face` that error put the dished
583/// face's volume contribution 4.69e-3 away from the closed form — the larger
584/// share of the case's 3.9435e-3 deficit.
585///
586/// Everything here is COMPUTED, never claimed. The offset radius comes from the
587/// same [`OffsetEvaluator`] the fit lane samples, so this lane cannot invent a
588/// sign convention; the guards below reject a centre the recognizer got wrong
589/// or a sphere turned inside out by an inward offset deeper than its radius;
590/// and the finished net is checked against that evaluator on a grid before it
591/// is returned. Every refusal falls through to the fit, which is what built
592/// these carriers before.
593///
594/// A sphere the recognizer does not recognize — a FITTED patch that merely
595/// looks spherical — stays on the fit lane, unchanged. This lane widens nothing
596/// on its own: cylinders, cones and tori want a scaling about an axis or a
597/// spine rather than a point, which is a different construction.
598fn sphere_offset_surface(face: &FaceRecord, distance: f64, reading: &mut Option<f64>) -> Result<Option<NurbsSurface>, KernelRefusal> {
599    // Escape hatch, the same shape as `BREP_NO_SPHERE_CHARTS`: it makes the
600    // closed form and the collocation fit two cells of one binary, so a
601    // measurement can separate this lane's effect from everything else.
602    if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
603        return Ok(None);
604    }
605    let source = &face.surface;
606    let Some((centre, radius)) = source.analytic().and_then(|shape| shape.sphere_geometry()) else {
607        return Ok(None);
608    };
609    let ([u0, u1], [v0, v1]) = domains(source)?;
610    // `offset_surface`'s positive distance moves OPPOSITE the face normal while
611    // the evaluator's moves ALONG it; the negation is the fit's own, reused
612    // here rather than re-derived (audit §4.1's hand negations get no new one).
613    // A normal this lane cannot read is a DECLINE, not an error: the fit below
614    // samples the same evaluator across the whole Greville grid and has its own
615    // singular-row recovery, so anything that offset before must still offset.
616    let Ok(sample) = OffsetEvaluator::new(
617        "offset_surface",
618        source,
619        OffsetNormal::FaceStable {
620            same_sense: face.same_sense,
621        },
622    )
623    .at((u0 + u1) / 2.0, (v0 + v1) / 2.0, -distance)
624    else {
625        return Ok(None);
626    };
627    let source_radial = sample.source.sub(centre);
628    let offset_radial = sample.point.sub(centre);
629    let offset_radius = offset_radial.length();
630    let scale = radius.max(1.0);
631    if radius <= 1e-12
632        || offset_radius <= 1e-9 * scale
633        // Offset through the centre and out the far side: the concentric mate
634        // is a sphere turned inside out, not this construction's answer.
635        || offset_radial.dot(source_radial) <= 0.0
636        // The move must be radial and exactly |d| long. A recognized centre
637        // that is not the real one fails here before it can scale anything.
638        || ((offset_radius - radius).abs() - distance.abs()).abs() > 1e-9 * scale
639    {
640        return Ok(None);
641    }
642    let factor = offset_radius / radius;
643    // A zero-weight control has no Euclidean point to scale. The fit lane never
644    // asks one for a position, so declining keeps this lane strictly additive.
645    let Ok(controls) = source
646        .control_points
647        .iter()
648        .map(|row| {
649            row.iter()
650                .map(|control| {
651                    Ok(Vec4::from_point(
652                        centre.add(control.point().or_refuse(KernelStage::Refine, "point")?.sub(centre).scale(factor)),
653                        control.w,
654                    ))
655                })
656                .collect::<Result<Vec<_>, KernelRefusal>>()
657        })
658        .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
659    else {
660        return Ok(None);
661    };
662    let Ok(surface) = NurbsSurface::new(
663        source.degree_u,
664        source.degree_v,
665        source.knots_u.clone(),
666        source.knots_v.clone(),
667        controls,
668    ) else {
669        return Ok(None);
670    };
671    // The claim, tested. A net that does not land on the pointwise offset
672    // everywhere is not this lane's surface, whatever it was recognized as —
673    // and a measurement that could not be taken is not a pass.
674    match crate::offset_measure::sphere_fit_reading(
675        source,
676        face.same_sense,
677        &surface,
678        distance,
679        1e-9 * scale,
680    ) {
681        Ok(measured) if measured.within_band() => {
682            *reading = Some(measured.deviation());
683            Ok(Some(surface))
684        },
685        _ => Ok(None),
686    }
687}
688
689/// The EXACT offset of a surface of REVOLUTION whose generatrix is ONE CIRCLE
690/// ARC — a torus of any kind (ring, horn, spindle), over a full or a partial
691/// sweep — or `None` for every other carrier.
692///
693/// Why it is exact. A revolution's normal lies in the meridian plane and equals
694/// the generatrix's own in-plane normal there, so the offset surface IS the
695/// revolution of the offset generatrix, and the offset of a circle arc of
696/// radius `r` about `c` is the concentric arc of radius `r ± d`. On the net:
697/// row `i` of a revolution's control net is an AFFINE image `A_i` of the
698/// generatrix's controls — a rotation for the on-circle rows, but a rotation
699/// STRETCHED radially by `1/cos` of the half-span for the mid rows, which is
700/// not a similarity. Affine maps commute with scaling about a point, so scaling
701/// row `i` about `C_i = A_i(c)` by `(r ± d)/r` is exactly `A_i` applied to the
702/// offset circle's net; that commutation is the only reason the stretched rows
703/// come out right. `C_i` is found without reading `A_i` at all: `c` is an affine
704/// combination of three generatrix controls, and the same combination of row
705/// `i`'s controls is `A_i(c)`. Knots, degrees and weights are untouched, so the
706/// same `(u, v)` still names the corresponding point and cloned trims land
707/// where they did, as on [`sphere_offset_surface`].
708///
709/// Everything is computed and then tested, never claimed. The generatrix is
710/// row 0 of the net, and it must lie on one circle to rounding. The sign and
711/// size of the move come from the same [`OffsetEvaluator`] the fit samples,
712/// read at a generatrix point against the circle's centre: the move must be
713/// radial and exactly `|d|` long, and an offset through the centre (`r − d ≤
714/// 0`, where the offset circle turns inside out) declines. The finished net
715/// must land on the pointwise offset everywhere on the measurement grid, to
716/// `1e-9` of the model's scale. Every decline falls through to the fit.
717///
718/// Measured on the partial lemon (a 106° arc of radius 5 centred 3 off the
719/// axis) shelled inward by 1: the fitted carrier sat 1.4e-5 off the pointwise
720/// offset and its shell 3.6e-7 short of the Pappus form in volume.
721fn revolved_arc_offset_surface(
722    face: &FaceRecord,
723    distance: f64,
724) -> Result<Option<NurbsSurface>, KernelRefusal> {
725    if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
726        return Ok(None);
727    }
728    let source = &face.surface;
729    match source.analytic() {
730        Some(crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. }) => {}
731        _ => return Ok(None),
732    }
733    let rows = &source.control_points;
734    let Some(first_row) = rows.first() else {
735        return Ok(None);
736    };
737    if rows.iter().flatten().any(|control| !(control.w > 0.0)) {
738        return Ok(None);
739    }
740    let Ok(generatrix) = NurbsCurve::new(source.degree_v, source.knots_v.clone(), first_row.clone())
741    else {
742        return Ok(None);
743    };
744    let [t0, t1] = generatrix.domain().or_refuse(KernelStage::Refine, "domain")?;
745    let at = |fraction: f64| generatrix.evaluate(t0 + (t1 - t0) * fraction);
746    // The circle through three generatrix points, then every sample held to it.
747    let (a, b, e) = (at(0.0).or_refuse(KernelStage::Refine, "at")?, at(1.0 / 3.0).or_refuse(KernelStage::Refine, "at")?, at(2.0 / 3.0).or_refuse(KernelStage::Refine, "at")?);
748    let (ab, ae) = (b.sub(a), e.sub(a));
749    let normal = ab.cross(ae);
750    let normal_length_squared = normal.dot(normal);
751    let Some(centre) = crate::circumcenter(a, b, e) else { return Ok(None); };
752    let radius = a.sub(centre).length();
753    let scale = (centre.length() + radius).max(1.0);
754    let band = 1e-9 * scale;
755    let unit_normal = normal.scale(1.0 / normal_length_squared.sqrt());
756    let samples = 16 * generatrix.control_points.len();
757    for index in 0..=samples {
758        let point = at(index as f64 / samples as f64).or_refuse(KernelStage::Refine, "at")?;
759        let radial = point.sub(centre);
760        if (radial.length() - radius).abs() > band || radial.dot(unit_normal).abs() > band {
761            return Ok(None);
762        }
763    }
764    // `centre` as an affine combination of three generatrix controls, the best
765    // conditioned pair against the first.
766    let controls = first_row
767        .iter()
768        .map(|control| control.point().or_refuse(KernelStage::Refine, "point"))
769        .collect::<Result<Vec<_>, KernelRefusal>>()?;
770    let origin = controls[0];
771    let mut best = (0usize, 0usize, 0.0f64);
772    for first in 1..controls.len() {
773        for second in first + 1..controls.len() {
774            let area = controls[first]
775                .sub(origin)
776                .cross(controls[second].sub(origin))
777                .length();
778            if area > best.2 {
779                best = (first, second, area);
780            }
781        }
782    }
783    let (first, second, area) = best;
784    if area <= 1e-9 * scale * scale {
785        return Ok(None);
786    }
787    let (along_first, along_second) = (controls[first].sub(origin), controls[second].sub(origin));
788    let offset_from_origin = centre.sub(origin);
789    let (g11, g12, g22) = (
790        along_first.dot(along_first),
791        along_first.dot(along_second),
792        along_second.dot(along_second),
793    );
794    let determinant = g11 * g22 - g12 * g12;
795    let (r1, r2) = (along_first.dot(offset_from_origin), along_second.dot(offset_from_origin));
796    let s = (r1 * g22 - r2 * g12) / determinant;
797    let t = (r2 * g11 - r1 * g12) / determinant;
798    if origin
799        .add(along_first.scale(s))
800        .add(along_second.scale(t))
801        .sub(centre)
802        .length()
803        > band
804    {
805        return Ok(None);
806    }
807    // The move, read off the shared evaluator on the generatrix row. The
808    // negation is the fit's own, as in `sphere_offset_surface`.
809    let ([u0, _], [v0, v1]) = domains(source)?;
810    let Ok(sample) = OffsetEvaluator::new(
811        "offset_surface",
812        source,
813        OffsetNormal::FaceStable {
814            same_sense: face.same_sense,
815        },
816    )
817    .at(u0, (v0 + v1) / 2.0, -distance)
818    else {
819        return Ok(None);
820    };
821    let source_radial = sample.source.sub(centre);
822    let offset_radial = sample.point.sub(centre);
823    let offset_radius = offset_radial.length();
824    if (source_radial.length() - radius).abs() > band
825        || offset_radius <= band
826        || offset_radial.dot(source_radial) <= 0.0
827        || ((offset_radius - radius).abs() - distance.abs()).abs() > band
828    {
829        return Ok(None);
830    }
831    let factor = offset_radius / radius;
832    let Ok(offset_rows) = rows
833        .iter()
834        .map(|row| {
835            let points = row.iter().map(|control| control.point().or_refuse(KernelStage::Refine, "point")).collect::<Result<Vec<_>, KernelRefusal>>()?;
836            let row_origin = points[0];
837            let row_centre = row_origin
838                .add(points[first].sub(row_origin).scale(s))
839                .add(points[second].sub(row_origin).scale(t));
840            Ok(points
841                .iter()
842                .zip(row)
843                .map(|(point, control)| {
844                    Vec4::from_point(row_centre.add(point.sub(row_centre).scale(factor)), control.w)
845                })
846                .collect::<Vec<_>>())
847        })
848        .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
849    else {
850        return Ok(None);
851    };
852    let Ok(surface) = NurbsSurface::new(
853        source.degree_u,
854        source.degree_v,
855        source.knots_u.clone(),
856        source.knots_v.clone(),
857        offset_rows,
858    ) else {
859        return Ok(None);
860    };
861    match measure_surface_fit_against_pointwise_offset(
862        source,
863        face.same_sense,
864        &surface,
865        distance,
866        band,
867    ) {
868        Ok(measured) if measured.within_band() => Ok(Some(surface)),
869        _ => Ok(None),
870    }
871}
872
873/// Lane O row 35's switch, default on since 2026-11-02 (batch 19; `0`/`off`
874/// restores the fit): a circular cylinder's offset carrier is built exactly
875/// ([`cylinder_offset_surface`]). The carve's certificate of the same factor
876/// is row 35a's own switch (`carriers.rs`).
877pub(crate) fn exact_cylinder_offsets_on() -> bool {
878    switch!("BREP_OFFSET_EXACT_CYLINDER", true)
879}
880
881
882/// The axis, radius and offset scale factor of a recognized CIRCULAR CYLINDER
883/// offset by `distance` (`offset_surface`'s sign: positive opposite the face
884/// normal), or `None` for every other carrier and for an offset through the
885/// axis. The move is read off the shared [`OffsetEvaluator`] at the middle of
886/// the domain, as in [`sphere_offset_surface`]: it must be radial and exactly
887/// `|d|` long, so this cannot invent a sign convention.
888pub(crate) fn cylinder_offset_factor(
889    surface: &NurbsSurface,
890    same_sense: bool,
891    distance: f64,
892) -> Result<Option<(Vec3, Vec3, f64, f64)>, KernelRefusal> {
893    if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
894        return Ok(None);
895    }
896    let Some(crate::AnalyticSurface::RuledRevolution { frame, rho0, rho1, height }) = surface.analytic() else {
897        return Ok(None);
898    };
899    let radius = 0.5 * (rho0 + rho1);
900    if !(radius > 0.0) || (rho0 - rho1).abs() > 1e-9 * radius.max(1.0) {
901        return Ok(None);
902    }
903    let Ok(axis) = frame.axis.normalized() else {
904        return Ok(None);
905    };
906    let origin = frame.origin;
907    let band = 1e-9 * (origin.length() + radius + height.abs()).max(1.0);
908    let perpendicular = |point: Vec3| {
909        let along = point.sub(origin);
910        along.sub(axis.scale(along.dot(axis)))
911    };
912    let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
913    let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
914    let Ok(sample) = OffsetEvaluator::new("offset_surface", surface, OffsetNormal::FaceStable { same_sense })
915        .at((u0 + u1) * 0.5, (v0 + v1) * 0.5, -distance)
916    else {
917        return Ok(None);
918    };
919    let (source_radial, offset_radial) = (perpendicular(sample.source), perpendicular(sample.point));
920    let offset_radius = offset_radial.length();
921    if (source_radial.length() - radius).abs() > band
922        || offset_radius <= band
923        || offset_radial.dot(source_radial) <= 0.0
924        || ((offset_radius - radius).abs() - distance.abs()).abs() > band
925        || sample.point.sub(sample.source).dot(axis).abs() > band
926    {
927        return Ok(None);
928    }
929    Ok(Some((origin, axis, radius, offset_radius / radius)))
930}
931
932/// Lane O row 35: the EXACT offset of a circular cylinder, or `None`.
933///
934/// A cylinder's normal is radial and constant along each ruling, so its
935/// pointwise offset is the coaxial cylinder of radius `R ± d`. Every column
936/// of the net (a rational circle) is an affine image of one polygon in a
937/// plane normal to the axis, and scaling about the axis by `(R ± d)/R` maps
938/// it to the same image of the offset circle; the rulings stay parallel to
939/// the axis. Knots, degrees and weights are untouched, so the same `(u, v)`
940/// names the corresponding point and cloned trims land where they did. The
941/// finished net must land on the pointwise offset of `reference` everywhere
942/// on the measurement grid, to `1e-9` of the model's scale; every decline
943/// falls through to the fit.
944fn cylinder_offset_surface(
945    reference: &NurbsSurface,
946    same_sense: bool,
947    distance: f64,
948) -> Result<Option<NurbsSurface>, KernelRefusal> {
949    let Some((origin, axis, radius, factor)) = cylinder_offset_factor(reference, same_sense, distance)? else {
950        return Ok(None);
951    };
952    if reference.control_points.iter().flatten().any(|control| !(control.w > 0.0)) {
953        return Ok(None);
954    }
955    let Ok(rows) = reference
956        .control_points
957        .iter()
958        .map(|row| {
959            row.iter()
960                .map(|control| {
961                    let point = control.point()?;
962                    let foot = origin.add(axis.scale(point.sub(origin).dot(axis)));
963                    Ok(Vec4::from_point(foot.add(point.sub(foot).scale(factor)), control.w))
964                })
965                .collect::<Result<Vec<_>, String>>()
966        })
967        .collect::<Result<Vec<Vec<_>>, String>>()
968    else {
969        return Ok(None);
970    };
971    let Ok(surface) = NurbsSurface::new(
972        reference.degree_u,
973        reference.degree_v,
974        reference.knots_u.clone(),
975        reference.knots_v.clone(),
976        rows,
977    ) else {
978        return Ok(None);
979    };
980    // The construction is exact once the recognizer has proved the cylinder
981    // and the evaluator has fixed the move, so this reads the finished net at
982    // 5 × 3 stations, both domain ends included, against the pointwise offset:
983    // a check of the build, not a measurement. `offset_surface_measured_*`
984    // still takes its full grid reading of this carrier, as of any fit.
985    let band = 1e-9 * (origin.length() + radius * factor.max(1.0)).max(1.0);
986    let evaluator = OffsetEvaluator::new("offset_surface", reference, OffsetNormal::FaceStable { same_sense });
987    let [u0, u1] = reference.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
988    let [v0, v1] = reference.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
989    for i in 0..=4 {
990        for j in 0..=2 {
991            let (u, v) = (u0 + (u1 - u0) * i as f64 / 4.0, v0 + (v1 - v0) * j as f64 / 2.0);
992            let (Ok(want), Ok(got)) = (evaluator.at(u, v, -distance), surface.evaluate(u, v)) else {
993                return Ok(None);
994            };
995            if !(want.point.sub(got).length() <= band) {
996                return Ok(None);
997            }
998        }
999    }
1000    Ok(Some(surface))
1001}
1002
1003pub(crate) fn offset_surface_with_lane(
1004    face: &FaceRecord,
1005    distance: f64,
1006    extension: &CarrierExtension,
1007) -> Result<(NurbsSurface, OffsetSurfaceLane), KernelRefusal> {
1008    offset_surface_constructed(face, distance, extension, &mut None).map(|(surface, lane, _)| (surface, lane))
1009}
1010
1011/// [`offset_surface_with_lane`], plus the surface the carrier is the pointwise
1012/// offset OF when that is not the face's own: the source extended along its
1013/// rulings, for a ruled extension (see the RULED EXTENSION block).
1014pub(crate) fn transpose_offset_surface(surface: &NurbsSurface) -> Result<NurbsSurface, KernelRefusal> {
1015    let controls = (0..surface.control_points[0].len())
1016        .map(|column| surface.control_points.iter().map(|row| row[column]).collect())
1017        .collect();
1018    NurbsSurface::new(surface.degree_v, surface.degree_u, surface.knots_v.clone(),
1019        surface.knots_u.clone(), controls).or_refuse(KernelStage::Refine, "offset_transpose")
1020}
1021
1022/// `BREP_OFFSET_APEX_END_REACH` (default on; `=0` restores): see the RULED EXTENSION
1023/// block of `offset_surface_constructed`.
1024pub(crate) fn apex_end_reach_enabled() -> bool {
1025    switch!("BREP_OFFSET_APEX_END_REACH", true)
1026}
1027
1028
1029fn offset_surface_constructed(
1030    face: &FaceRecord,
1031    distance: f64,
1032    extension: &CarrierExtension,
1033    sphere_reading: &mut Option<f64>,
1034) -> Result<(NurbsSurface, OffsetSurfaceLane, Option<NurbsSurface>), KernelRefusal> {
1035    let source = &face.surface;
1036    let extension_active = extension.is_active();
1037    // Imported cones can put the ruling in u. Normalize that chart for the
1038    // existing exact ruling/pinch construction, then restore the caller's UV.
1039    // Transposition reverses Su x Sv, so the face sense reverses with it.
1040    // The recognizer must prove a ruled revolution; arbitrary lofts keep
1041    // their existing evaluator and fit semantics.
1042    let normalize_ruling = std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0")
1043        && source.degree_u == 1 && source.control_points.len() == 2;
1044    let normalize_sphere = std::env::var("BREP_OFFSET_FOLD_CHART").as_deref() != Ok("0")
1045        && source.degree_u == 2 && source.degree_v == 2 && source.analytic().is_none();
1046    if normalize_ruling || normalize_sphere {
1047        let transposed = transpose_offset_surface(source)?;
1048        let ruled = normalize_ruling && matches!(transposed.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. }));
1049        let spherical = normalize_sphere && transposed.analytic().and_then(|shape| shape.sphere_geometry()).is_some();
1050        if ruled || spherical {
1051            let mut turned = face.clone();
1052            turned.surface = transposed;
1053            turned.same_sense = !turned.same_sense;
1054            for coedge in turned.loops.iter_mut().flat_map(|lp| &mut lp.coedges) {
1055                let controls = coedge.pcurve.control_points.iter().map(|point| {
1056                    let mut swapped = *point;
1057                    std::mem::swap(&mut swapped.x, &mut swapped.y);
1058                    swapped
1059                }).collect();
1060                coedge.pcurve = NurbsCurve::new(coedge.pcurve.degree,
1061                    coedge.pcurve.knots.clone(), controls).or_refuse(KernelStage::Refine, "offset_transpose_pcurve")?;
1062            }
1063            let turned_extension = CarrierExtension {
1064                u_min: extension.v_min, u_max: extension.v_max,
1065                v_min: extension.u_min, v_max: extension.u_max,
1066            };
1067            let (surface, lane, extended) = offset_surface_constructed(&turned, distance, &turned_extension, &mut None)?;
1068            return Ok((transpose_offset_surface(&surface)?, lane,
1069                extended.as_ref().map(transpose_offset_surface).transpose()?));
1070        }
1071    }
1072    if source.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
1073        let ([u0, u1], [v0, v1]) = domains(source)?;
1074        let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
1075        let shift = normal.scale(-distance);
1076        let mut points = source
1077            .control_points
1078            .iter()
1079            .map(|row| {
1080                row.iter()
1081                    .map(|control| Ok(control.point().or_refuse(KernelStage::Refine, "point")?.add(shift)))
1082                    .collect::<Result<Vec<_>, KernelRefusal>>()
1083            })
1084            .collect::<Result<Vec<_>, KernelRefusal>>()?;
1085        if extension_active {
1086            let p00 = points[0][0];
1087            let p01 = points[0][1];
1088            let p10 = points[1][0];
1089            let direction_u = p10.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
1090            let direction_v = p01.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
1091            // The net slides while the cloned pcurves stay put, so a trim
1092            // that spans only part of the domain would grow by less than
1093            // asked (a 20-wide face trimmed to [0,16] by a blend moved its
1094            // x=16 edge 0.6 for a 1.0 pad). Solve the slide for the TRIM's
1095            // own bounding box instead.
1096            let (trim_u, trim_v) = trim_domain_fractions(face, [u0, u1], [v0, v1])?;
1097            let (back_u, forward_u) = domain_end_moves(extension.u_min, extension.u_max, trim_u);
1098            let (back_v, forward_v) = domain_end_moves(extension.v_min, extension.v_max, trim_v);
1099            points[0][0] = p00
1100                .sub(direction_u.scale(back_u))
1101                .sub(direction_v.scale(back_v));
1102            points[0][1] = p01
1103                .sub(direction_u.scale(back_u))
1104                .add(direction_v.scale(forward_v));
1105            points[1][0] = p10
1106                .add(direction_u.scale(forward_u))
1107                .sub(direction_v.scale(back_v));
1108            points[1][1] = points[1][1]
1109                .add(direction_u.scale(forward_u))
1110                .add(direction_v.scale(forward_v));
1111        }
1112        let controls = points
1113            .into_iter()
1114            .enumerate()
1115            .map(|(row, points)| {
1116                points
1117                    .into_iter()
1118                    .enumerate()
1119                    .map(|(column, point)| {
1120                        Vec4::from_point(point, source.control_points[row][column].w)
1121                    })
1122                    .collect()
1123            })
1124            .collect();
1125        // The extension slides the control net along the plane, so the same
1126        // `(u, v)` no longer names the pointwise offset of the same source
1127        // point; without it the shift is rigid and exact.
1128        let lane = if extension_active {
1129            OffsetSurfaceLane::Reparameterised
1130        } else {
1131            OffsetSurfaceLane::Affine
1132        };
1133        return Ok((
1134            NurbsSurface::new(
1135                source.degree_u,
1136                source.degree_v,
1137                source.knots_u.clone(),
1138                source.knots_v.clone(),
1139                controls,
1140            ).or_refuse(KernelStage::Refine, "new")?,
1141            lane,
1142            None,
1143        ));
1144    }
1145
1146    // A recognized SPHERE has a closed-form concentric offset that needs no
1147    // collocation at all. Taken before the fit so the exact answer wins
1148    // wherever it exists; `sphere_offset_surface` declines on everything else,
1149    // and on a sphere whose construction it cannot verify.
1150    //
1151    // The extension cannot reach a sphere either way: both blocks below sit
1152    // under `parameters_v.len() == 2`, and a sphere's meridian is a degree-2
1153    // net of five controls. Taking this lane under an active extension keeps
1154    // exactly the behaviour the fit had — no growth — and only sharpens it.
1155    if let Some(surface) = sphere_offset_surface(face, distance, sphere_reading)? {
1156        return Ok((surface, OffsetSurfaceLane::Analytic, None));
1157    }
1158    // The same holds for a revolved ARC: its generatrix has three or more
1159    // controls, so no extension below could reach it.
1160    if let Some(surface) = revolved_arc_offset_surface(face, distance)? {
1161        return Ok((surface, OffsetSurfaceLane::Analytic, None));
1162    }
1163
1164    let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u).or_refuse(KernelStage::Refine, "new")?;
1165    let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v).or_refuse(KernelStage::Refine, "new")?;
1166    let parameters_u = greville_parameters(&knot_u);
1167    let parameters_v = greville_parameters(&knot_v);
1168    // The Greville sample grid IS a pointwise offset evaluation — this fit is
1169    // the shared evaluator's consumer, not its peer. `offset_surface`'s
1170    // positive distance moves OPPOSITE the face normal while the evaluator's
1171    // moves ALONG it, so the negation happens once, here, with a name on it
1172    // (audit §4.1's four hand negations get no fifth).
1173    let evaluator = OffsetEvaluator::new(
1174        "offset_surface",
1175        source,
1176        OffsetNormal::FaceStable {
1177            same_sense: face.same_sense,
1178        },
1179    );
1180    let mut samples = Vec::new();
1181    for &u in &parameters_u {
1182        let mut row = Vec::new();
1183        for &v in &parameters_v {
1184            row.push(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point);
1185        }
1186        samples.push(row);
1187    }
1188    // APEX-CONE PINCH RETRIM: offsetting an apex cone INWARD moves each
1189    // ruling past the axis — the sampled far row becomes a ring on the far
1190    // side (radius d·cos half-angle, mirrored through the axis) and the
1191    // offset surface self-pinches inside the v-domain. The genuine cavity
1192    // ends AT the pinch (the offset cone's own apex). For a linear-v net
1193    // (two sample rows — every made/booleaned cone) the pinch lies on each
1194    // ruling at the fraction where the radial vector vanishes: detect the
1195    // inversion (far-row radials anti-parallel to near-row radials about the
1196    // row centroids) and pull the far row back to the pinch point, so the
1197    // fitted surface ends in a proper degenerate apex row instead of a
1198    // parasitic inverted tip ending in an unweldable ring.
1199    // Both blocks below move the sample grid OFF the pointwise offset on
1200    // purpose. Recording that is what lets `offset_surface_measured` decline to
1201    // report a designed divergence as a fit error.
1202    let mut reparameterised = false;
1203    let mut extended_source: Option<NurbsSurface> = None;
1204    // Two u samples are enough for both: a 2 × 2 bilinear net (a planar
1205    // trapezoid wall of a lofted square frustum, which the affine test above
1206    // rejects on its parallelogram defect) has straight rulings like any
1207    // other linear-v net, and without the extension its inward carrier
1208    // stopped 0.059 short of the opening the reach lane had measured
1209    // (2026-09-26, the SHARP rim corner fixture).
1210    if parameters_v.len() == 2 && parameters_u.len() >= 2 {
1211        let centroid = |column: usize| {
1212            let mut sum = Vec3::default();
1213            for row in &samples {
1214                sum = sum.add(row[column]);
1215            }
1216            sum.scale(1.0 / samples.len() as f64)
1217        };
1218        let near_centroid = centroid(0);
1219        let far_centroid = centroid(1);
1220        let mut inverted = true;
1221        let mut pinch_fraction = 0.0f64;
1222        let mut near_mean = 0.0f64;
1223        let mut far_mean = 0.0f64;
1224        for row in &samples {
1225            let near_radial = row[0].sub(near_centroid);
1226            let far_radial = row[1].sub(far_centroid);
1227            let near_len = near_radial.length();
1228            let far_len = far_radial.length();
1229            if near_len <= 1e-9 || far_len <= 1e-9 {
1230                inverted = false;
1231                break;
1232            }
1233            if near_radial.dot(far_radial) >= 0.0 {
1234                inverted = false;
1235                break;
1236            }
1237            pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
1238            near_mean += near_len / samples.len() as f64;
1239            far_mean += far_len / samples.len() as f64;
1240        }
1241        if inverted {
1242            let retrim_far = match pinch_side_by_source(source, &parameters_u, &parameters_v, &samples, near_centroid, far_centroid)? {
1243                Some(far) => far,
1244                None => far_mean <= near_mean,
1245            };
1246            // WHOSE fold is it?  The retrim exists so a cavity that ends at the
1247            // offset cone's own apex ends there in the TOPOLOGY too, instead of
1248            // running past the pinch into a mirrored ring no weld can use.  It
1249            // pays for that by moving the sample grid off the pointwise offset,
1250            // and the price is the shared-basis identity every consumer rests
1251            // on: after it, the SAME `(u, v)` no longer names the offset of the
1252            // same source point.
1253            //
1254            // That price is only worth paying when the face's TRIM actually
1255            // reaches past the pinch.  A trim on a cone's wide half offsets
1256            // perfectly well; its carrier folds somewhere the face never goes,
1257            // and shearing its parameterization to tidy that away sheared the
1258            // trim's own pcurves with it — a 45° cone sheet trimmed to its wide
1259            // half shipped a slab 0.86 % off its Pappus volume, validating and
1260            // wrong (`thicken.rs`,
1261            // `a_re_parameterized_offset_carrier_keeps_the_pointwise_offset`).
1262            //
1263            // So read the trim, the way the extension below already does.  With
1264            // a linear v basis the pinch sits at the v FRACTION
1265            // `pinch_fraction`, and the folded side is whichever end the retrim
1266            // would have pulled.  A face with no loops still reports the whole
1267            // domain, which keeps every full-domain carrier on its exact
1268            // previous path.
1269            let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1270            let trim_reaches_the_pinch = if retrim_far {
1271                trim_v[1] > pinch_fraction + 1e-9
1272            } else {
1273                trim_v[0] < pinch_fraction - 1e-9
1274            };
1275            if trim_reaches_the_pinch {
1276                // Pull the crossed (smaller-ring, past-the-pinch) end back to
1277                // the pinch point on each ruling.
1278                reparameterised = true;
1279                for row in &mut samples {
1280                    let near = row[0];
1281                    let far = row[1];
1282                    let pinch = near.add(far.sub(near).scale(pinch_fraction));
1283                    if retrim_far {
1284                        row[1] = pinch;
1285                    } else {
1286                        row[0] = pinch;
1287                    }
1288                }
1289            }
1290        }
1291        // RULED EXTENSION: `planar_extension` is a no-op for curved carriers
1292        // above, but a cone/cylinder lateral joined at a reflex edge needs
1293        // its offset skin to GROW past the source rim exactly like a plane
1294        // (a cylinder piercing a cone: the two offsets only meet past both
1295        // cloned rims). The carrier's parameterization stays (knots/pcurves
1296        // untouched) while its world image — and with it the cloned trim's
1297        // image — inflates: the carrier is the pointwise offset of the SOURCE
1298        // EXTENDED ALONG ITS RULINGS, which a linear-v net does exactly.
1299        //
1300        // Stretching the sampled OFFSET rulings instead reaches the same
1301        // surface only where the source is DEVELOPABLE — its normal constant
1302        // along each ruling, as on a cylinder, a cone or an extruded wall —
1303        // because only there is the offset ruled too. A twisted loft is ruled
1304        // and not developable: its stretched offset rulings left the offset,
1305        // and an outward shell of one validated 0.235 short (1.0 %). So the
1306        // stretch is MEASURED against the extended source's pointwise offset,
1307        // at both ends and the middle of every sampled ruling, and kept only
1308        // within the fit contract; otherwise the extended source is sampled
1309        // pointwise, and either way the refinement reads the carrier against it.
1310        if extension.extends_v() && !inverted {
1311            // `BREP_OFFSET_APEX_END_REACH` (default on; `=0` restores): a sheaf whose apex
1312            // lies exactly ON an end row (a cone made to its point: the source
1313            // rows read apex_at = 1) is not a sheet folded inside its span; it
1314            // is grown away from the apex and not toward it. Without it the
1315            // whole sheet read unextendable, so a cone grown by 1 kept its
1316            // base ring at z = 0.6 above the grown base plane z = -1, the pair
1317            // was skipped on bounds and the ring stayed one-use
1318            // (brep-bench offset/cone-grown, non-integral genus).
1319            let apex_end_reach = apex_end_reach_enabled();
1320            let mut min_ruling = f64::MAX;
1321            let mut back_allowance = f64::MAX;
1322            let mut forward_allowance = f64::MAX;
1323            let mut extendable = true;
1324            for row in &samples {
1325                let ruling = row[1].sub(row[0]);
1326                let length = ruling.length();
1327                min_ruling = min_ruling.min(length);
1328                // Radii about the row centroids expose a converging (conic)
1329                // ruling sheaf; the extension must stop short of its apex or
1330                // the sheet folds through it.
1331                let near_radial = row[0].sub(near_centroid).length();
1332                let far_radial = row[1].sub(far_centroid).length();
1333                if (far_radial - near_radial).abs() > 1e-9 {
1334                    let apex_at = near_radial / (near_radial - far_radial);
1335                    if apex_end_reach && apex_at.abs() <= 1e-9 {
1336                        back_allowance = 0.0;
1337                        continue;
1338                    }
1339                    if apex_end_reach && (apex_at - 1.0).abs() <= 1e-9 {
1340                        forward_allowance = 0.0;
1341                        continue;
1342                    }
1343                    if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1344                        // Apex inside the span: degenerate sheet, do not touch.
1345                        extendable = false;
1346                        break;
1347                    }
1348                    if apex_at < 0.0 {
1349                        back_allowance = back_allowance.min(0.9 * -apex_at);
1350                    } else {
1351                        forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
1352                    }
1353                }
1354            }
1355            // The allowances above are read off the OFFSET rows. The source's
1356            // own ruling sheaf converges at a different rate — an outward
1357            // offset moves a cone's apex FARTHER along the ruling — so a
1358            // growth the offset rows allow can still carry the EXTENDED SOURCE
1359            // through its own apex, and its pointwise offset would then be
1360            // taken on a surface folded back on itself. Read the same radial
1361            // ratio on the source rows and clamp by the tighter of the two.
1362            let mut source_back = f64::MAX;
1363            let mut source_forward = f64::MAX;
1364            if extendable {
1365                let source_centroid = |column: usize| {
1366                    let mut sum = Vec3::default();
1367                    for row in &source.control_points {
1368                        if let Ok(point) = row[column].point() {
1369                            sum = sum.add(point);
1370                        }
1371                    }
1372                    sum.scale(1.0 / source.control_points.len() as f64)
1373                };
1374                let (near_source, far_source) = (source_centroid(0), source_centroid(1));
1375                for row in &source.control_points {
1376                    let (near, far) = (row[0].point().or_refuse(KernelStage::Refine, "point")?, row[1].point().or_refuse(KernelStage::Refine, "point")?);
1377                    let near_radial = near.sub(near_source).length();
1378                    let far_radial = far.sub(far_source).length();
1379                    if (far_radial - near_radial).abs() > 1e-9 {
1380                        let apex_at = near_radial / (near_radial - far_radial);
1381                        if apex_end_reach && apex_at.abs() <= 1e-9 {
1382                            source_back = 0.0;
1383                            continue;
1384                        }
1385                        if apex_end_reach && (apex_at - 1.0).abs() <= 1e-9 {
1386                            source_forward = 0.0;
1387                            continue;
1388                        }
1389                        if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1390                            extendable = false;
1391                            break;
1392                        }
1393                        if apex_at < 0.0 {
1394                            source_back = source_back.min(0.9 * -apex_at);
1395                        } else {
1396                            source_forward = source_forward.min(0.9 * (apex_at - 1.0));
1397                        }
1398                    }
1399                }
1400            }
1401            let (back_allowance, forward_allowance) =
1402                (back_allowance.min(source_back), forward_allowance.min(source_forward));
1403            if extendable && min_ruling > 1e-9 {
1404                // Solved for the TRIM's v-extent, not the domain's: a bore
1405                // face keeps its drill's full-height surface and is trimmed
1406                // to the part it pierces, so moving the surface's own ends
1407                // by |d| moved the trim's rims by only a fraction of that
1408                // (a 30-long ruling trimmed to 20 gave 0.667 for 1.0) — and
1409                // an outward shell's bore never reached the grown planes.
1410                let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1411                let (back_world, forward_world) =
1412                    domain_end_moves(extension.v_min, extension.v_max, trim_v);
1413                let (requested_back, requested_forward) = (back_world / min_ruling, forward_world / min_ruling);
1414                // APEX END: the 0.9 above keeps a growth from folding through an
1415                // apex it might not have located exactly. When the source is an
1416                // analytic cone the offset apex IS located exactly, and a reach
1417                // that would pass it ends AT it: the far row becomes the pole the
1418                // cavity really ends in. Stopping at 0.9 left a ring there (radius
1419                // 2.99e-3 on the d = 1 inward cone) that the carrier's rim
1420                // collapse then named a point, so the shipped edge was a point on
1421                // a trim that is a ring, and the shell was open over its disk —
1422                // 2.69e-5 of volume, all of it. Only while the SOURCE's own apex
1423                // lies beyond (its clamped allowance covers the reach), so the
1424                // extended source never folds; otherwise the clamp above stands.
1425                let apex_end = analytic_offset_apex(source, &samples, offset_fit_contract(source)).and_then(
1426                    |(at, apex)| {
1427                        if at > 1.0 && at - 1.0 <= requested_forward && at - 1.0 <= source_forward {
1428                            Some((false, at - 1.0, apex))
1429                        } else if at < 0.0 && -at <= requested_back && -at <= source_back {
1430                            Some((true, -at, apex))
1431                        } else {
1432                            None
1433                        }
1434                    },
1435                );
1436                let back = match apex_end {
1437                    Some((true, reach, _)) => reach,
1438                    _ => requested_back.min(back_allowance),
1439                };
1440                let forward = match apex_end {
1441                    Some((false, reach, _)) => reach,
1442                    _ => requested_forward.min(forward_allowance),
1443                };
1444                let stretched = samples
1445                    .iter()
1446                    .map(|row| {
1447                        let ruling = row[1].sub(row[0]);
1448                        let (near, far) = (row[0].sub(ruling.scale(back)), row[1].add(ruling.scale(forward)));
1449                        match apex_end {
1450                            Some((true, _, apex)) => vec![apex, far],
1451                            Some((false, _, apex)) => vec![near, apex],
1452                            None => vec![near, far],
1453                        }
1454                    })
1455                    .collect::<Vec<_>>();
1456                let extended = extend_along_rulings(source, back, forward)?;
1457                let extended_offsets = OffsetEvaluator::new(
1458                    "offset_surface",
1459                    &extended,
1460                    OffsetNormal::FaceStable {
1461                        same_sense: face.same_sense,
1462                    },
1463                );
1464                let [v0, v1] = knot_v.domain();
1465                let mut developability = 0.0f64;
1466                for (row, &u) in stretched.iter().zip(&parameters_u) {
1467                    for fraction in [0.0, 0.5, 1.0] {
1468                        let exact = extended_offsets.at(u, v0 + (v1 - v0) * fraction, -distance).or_refuse(KernelStage::Refine, "at")?.point;
1469                        let along = row[0].add(row[1].sub(row[0]).scale(fraction));
1470                        developability = developability.max(exact.sub(along).length());
1471                    }
1472                }
1473                let contract = offset_fit_contract(source);
1474                let stretch_kept = developability <= contract;
1475                samples = if stretch_kept {
1476                    stretched
1477                } else {
1478                    parameters_u
1479                        .iter()
1480                        .map(|&u| {
1481                            parameters_v
1482                                .iter()
1483                                .map(|&v| Ok(extended_offsets.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1484                                .collect::<Result<Vec<_>, KernelRefusal>>()
1485                        })
1486                        .collect::<Result<Vec<_>, KernelRefusal>>()?
1487                };
1488                if let Ok(path) = std::env::var("BREP_OFFSET_FIT_TRACE") {
1489                    use std::io::Write;
1490                    let line = format!(
1491                        "extension deg=({},{}) net=({},{}) d={distance:.6} back={back:.6} forward={forward:.6} \
1492                         developability={developability:.3e} contract={contract:.3e} lane={} thread={} args={}\n",
1493                        source.degree_u,
1494                        source.degree_v,
1495                        source.control_points.len(),
1496                        source.control_points[0].len(),
1497                        if stretch_kept { "stretch" } else { "pointwise" },
1498                        std::thread::current().name().unwrap_or("-"),
1499                        std::env::args().skip(1).collect::<Vec<_>>().join(" "),
1500                    );
1501                    if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1502                        let _ = file.write_all(line.as_bytes());
1503                    }
1504                }
1505                extended_source = Some(extended);
1506            }
1507        }
1508    }
1509    let reference = extended_source.as_ref().unwrap_or(source);
1510    // Lane O row 35 (`BREP_OFFSET_EXACT_CYLINDER`, default on): a CIRCULAR
1511    // CYLINDER's offset is the coaxial cylinder, built exactly from the
1512    // reference the extension above settled on (so every growth decision is
1513    // the fit's own) and measured before it is kept; any decline falls through
1514    // to the fit below, unchanged.
1515    if !reparameterised && exact_cylinder_offsets_on() {
1516        if let Some(surface) = cylinder_offset_surface(reference, face.same_sense, distance)? {
1517            return Ok((surface, OffsetSurfaceLane::Analytic, extended_source));
1518        }
1519    }
1520    let weights = reference
1521        .control_points
1522        .iter()
1523        .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1524        .collect::<Vec<_>>();
1525    let fitted = NurbsSurface::new(
1526        source.degree_u,
1527        source.degree_v,
1528        source.knots_u.clone(),
1529        source.knots_v.clone(),
1530        interpolate_tensor(
1531            &knot_u,
1532            &knot_v,
1533            &parameters_u,
1534            &parameters_v,
1535            &samples,
1536            &weights,
1537        )?,
1538    ).or_refuse(KernelStage::Refine, "new")?;
1539    if reparameterised {
1540        return Ok((fitted, OffsetSurfaceLane::Reparameterised, None));
1541    }
1542    let reference_offsets = OffsetEvaluator::new(
1543        "offset_surface",
1544        reference,
1545        OffsetNormal::FaceStable {
1546            same_sense: face.same_sense,
1547        },
1548    );
1549    Ok((
1550        refine_offset_fit(reference, &reference_offsets, distance, fitted)?,
1551        OffsetSurfaceLane::Fit,
1552        extended_source,
1553    ))
1554}
1555
1556
1557/// Where the sampled OFFSET rulings of an analytic cone or frustum meet its
1558/// axis: the ruling fraction (past 1 beyond the far row, below 0 before the near
1559/// row) and the point. The radial ratio above already gives the fraction (both
1560/// rows and their centroids scale about the apex); what it does not give is the
1561/// POINT the far row must become, so each ruling is met with the recognized axis
1562/// here and the crossings are checked to be one point. `None` unless the source
1563/// is a `RuledRevolution` and every ruling meets the axis within `contract`,
1564/// within `contract` of one point.
1565fn analytic_offset_apex(source: &NurbsSurface, samples: &[Vec<Vec3>], contract: f64) -> Option<(f64, Vec3)> {
1566    let Some(crate::AnalyticSurface::RuledRevolution { frame, .. }) = crate::analytic_surface::recognize(source) else {
1567        return None;
1568    };
1569    let radial = |point: Vec3| {
1570        let offset = point.sub(frame.origin);
1571        offset.sub(frame.axis.scale(offset.dot(frame.axis)))
1572    };
1573    let mut crossings = Vec::with_capacity(samples.len());
1574    for row in samples {
1575        let (near, far) = (radial(row[0]), radial(row[1]));
1576        let step = far.sub(near);
1577        let at = -near.dot(step) / step.dot(step);
1578        if !at.is_finite() {
1579            return None;
1580        }
1581        crossings.push((at, row[0].add(row[1].sub(row[0]).scale(at))));
1582    }
1583    // Averaged as offsets from the first crossing, so a part far from the world
1584    // origin sums small differences, not large coordinates.
1585    let count = crossings.len() as f64;
1586    let at = crossings.iter().map(|(at, _)| at).sum::<f64>() / count;
1587    let first = crossings.first()?.1;
1588    let spread = crossings.iter().fold(Vec3::default(), |sum, (_, point)| sum.add(point.sub(first)));
1589    let centre = first.add(spread.scale(1.0 / count));
1590    let apex = frame.origin.add(frame.axis.scale(centre.sub(frame.origin).dot(frame.axis)));
1591    // On the axis, and one point: a cylinder's near-parallel rulings give a
1592    // finite `at` from rounding, and only these two readings refuse it.
1593    crossings
1594        .iter()
1595        .all(|(_, point)| radial(*point).length() <= contract && point.sub(apex).length() <= contract)
1596        .then_some((at, apex))
1597}
1598
1599/// A linear-v surface continued along its own rulings by `back` and `forward`
1600/// ruling lengths — the same surface, re-parameterized onto the longer span.
1601/// Exact: each ruling is linear in homogeneous coordinates, so extrapolating the
1602/// homogeneous control rows traces the ruling past both ends. Refused where a
1603/// continued weight stops being positive, which is where the ruling leaves the
1604/// rational patch.
1605fn extend_along_rulings(surface: &NurbsSurface, back: f64, forward: f64) -> Result<NurbsSurface, KernelRefusal> {
1606    let rows = surface
1607        .control_points
1608        .iter()
1609        .map(|row| {
1610            let step = row[1].add(row[0].scale(-1.0));
1611            vec![row[0].add(step.scale(-back)), row[1].add(step.scale(forward))]
1612        })
1613        .collect::<Vec<_>>();
1614    if rows.iter().flatten().any(|point| point.w <= 1e-12) {
1615        return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_ruled_extension_weight", format!(
1616            "offset_surface: extending a ruled face by ({back:.6}, {forward:.6}) of its rulings \
1617             carries a weight through zero; the extended face is not this surface"
1618        )));
1619    }
1620    NurbsSurface::new(surface.degree_u, 1, surface.knots_u.clone(), surface.knots_v.clone(), rows)
1621        .or_refuse(KernelStage::Refine, "new")
1622}
1623
1624/// The fit contract a carrier of `source` is refined toward: the kernel's
1625/// intersection-fit tolerance for the source net's extent.
1626fn offset_fit_contract(source: &NurbsSurface) -> f64 {
1627    let scale = crate::model_scale(
1628        source
1629            .control_points
1630            .iter()
1631            .flatten()
1632            .filter_map(|point| point.point().ok()),
1633    );
1634    crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit
1635}
1636
1637/// Halvings of every knot span [`refine_offset_fit`] may take, per direction.
1638const OFFSET_FIT_REFINEMENTS: usize = 6;
1639
1640/// The largest net one refined collocation may solve: separable weights solve
1641/// one direction at a time, non-separable ones solve the whole tensor densely.
1642const OFFSET_FIT_MAX_CONTROLS_SEPARABLE: usize = 16_384;
1643const OFFSET_FIT_MAX_CONTROLS_TENSOR: usize = 900;
1644
1645/// The out-of-sample deviation of a collocation fit from the pointwise offset,
1646/// read at the MIDPOINTS between consecutive Greville parameters, where the fit
1647/// interpolates nothing: along u (u midpoints on the v Greville rows), along v,
1648/// and between both.
1649#[derive(Clone, Debug)]
1650struct OffsetFitDeviation {
1651    along_u: f64,
1652    along_v: f64,
1653    between: f64,
1654    u_sites: Vec<(f64, f64)>,
1655    v_sites: Vec<(f64, f64)>,
1656    between_uv: (f64, f64),
1657}
1658
1659impl OffsetFitDeviation {
1660    fn worst(&self) -> f64 {
1661        self.along_u.max(self.along_v).max(self.between)
1662    }
1663
1664    fn refinement_sites(&self, bar: f64) -> (Vec<f64>, Vec<f64>) {
1665        let mut u = self.u_sites.iter().filter(|(_, miss)| *miss > bar)
1666            .map(|(parameter, _)| *parameter).collect::<Vec<_>>();
1667        let mut v = self.v_sites.iter().filter(|(_, miss)| *miss > bar)
1668            .map(|(parameter, _)| *parameter).collect::<Vec<_>>();
1669        if u.is_empty() && v.is_empty() && self.between > bar {
1670            u.push(self.between_uv.0);
1671            v.push(self.between_uv.1);
1672        }
1673        (u, v)
1674    }
1675}
1676
1677fn greville_midpoints(parameters: &[f64]) -> Vec<f64> {
1678    parameters
1679        .windows(2)
1680        .filter(|pair| pair[1] - pair[0] > 1e-12)
1681        .map(|pair| 0.5 * (pair[0] + pair[1]))
1682        .collect()
1683}
1684
1685fn offset_fit_deviation(
1686    fitted: &NurbsSurface,
1687    evaluator: &OffsetEvaluator,
1688    distance: f64,
1689) -> Result<OffsetFitDeviation, KernelRefusal> {
1690    let parameters_u = greville_parameters(&KnotVector::new(fitted.knots_u.clone(), fitted.degree_u).or_refuse(KernelStage::Refine, "new")?);
1691    let parameters_v = greville_parameters(&KnotVector::new(fitted.knots_v.clone(), fitted.degree_v).or_refuse(KernelStage::Refine, "new")?);
1692    let (middle_u, middle_v) = (greville_midpoints(&parameters_u), greville_midpoints(&parameters_v));
1693    let residual_locus = std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0");
1694    let gap = |u: f64, v: f64| -> Result<f64, KernelRefusal> {
1695        Ok(fitted.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?.sub(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point).length())
1696    };
1697    let mut deviation = OffsetFitDeviation {
1698        along_u: 0.0,
1699        along_v: 0.0,
1700        between: 0.0,
1701        u_sites: Vec::new(),
1702        v_sites: if residual_locus { middle_v.iter().map(|&v| (v, 0.0)).collect() } else { Vec::new() },
1703        between_uv: (0.0, 0.0),
1704    };
1705    for &u in &middle_u {
1706        let mut along = 0.0f64;
1707        for &v in &parameters_v {
1708            let miss = gap(u, v)?;
1709            along = along.max(miss);
1710            deviation.along_u = deviation.along_u.max(miss);
1711        }
1712        if residual_locus { deviation.u_sites.push((u, along)); }
1713        for &v in &middle_v {
1714            let miss = gap(u, v)?;
1715            if miss > deviation.between { deviation.between_uv = (u, v); }
1716            deviation.between = deviation.between.max(miss);
1717        }
1718    }
1719    for &u in &parameters_u {
1720        for (index, &v) in middle_v.iter().enumerate() {
1721            let miss = gap(u, v)?;
1722            deviation.along_v = deviation.along_v.max(miss);
1723            if residual_locus { deviation.v_sites[index].1 = deviation.v_sites[index].1.max(miss); }
1724        }
1725    }
1726    Ok(deviation)
1727}
1728
1729/// `surface` with each requested LINEAR direction raised to cubic, the same
1730/// geometry: every span re-expressed as a cubic Bezier on the homogeneous net
1731/// (the points at thirds), joined C0 by triple knots. `None` when a requested
1732/// direction is not linear or carries a repeated interior knot.
1733///
1734/// A linear direction is raised rather than halved because the offset of a
1735/// ruled surface is generally not ruled, and a piecewise-linear fit converges
1736/// only as h^2: the twisted loft's v direction still read 3.3e-6 after six
1737/// halvings, where the cubic basis converges as h^4.
1738fn raise_linear_directions(
1739    surface: &NurbsSurface,
1740    in_u: bool,
1741    in_v: bool,
1742) -> Result<Option<NurbsSurface>, KernelRefusal> {
1743    fn raise(knots: &[f64], row: &[Vec4]) -> Option<(Vec<f64>, Vec<Vec4>)> {
1744        // Degree 1: knots are [k0, k0, k1, ..., kn, kn] for n + 1 controls.
1745        let count = row.len();
1746        if knots.len() != count + 2 {
1747            return None;
1748        }
1749        let breaks = &knots[1..=count];
1750        if breaks.windows(2).any(|pair| pair[1] - pair[0] <= 1e-12) {
1751            return None;
1752        }
1753        let mut raised_knots = vec![breaks[0]; 4];
1754        for &interior in &breaks[1..count - 1] {
1755            raised_knots.extend([interior; 3]);
1756        }
1757        raised_knots.extend([breaks[count - 1]; 4]);
1758        let mut points = Vec::with_capacity(3 * (count - 1) + 1);
1759        for pair in row.windows(2) {
1760            let step = pair[1].add(pair[0].scale(-1.0));
1761            points.push(pair[0]);
1762            points.push(pair[0].add(step.scale(1.0 / 3.0)));
1763            points.push(pair[0].add(step.scale(2.0 / 3.0)));
1764        }
1765        points.push(row[count - 1]);
1766        Some((raised_knots, points))
1767    }
1768    let mut knots_u = surface.knots_u.clone();
1769    let mut knots_v = surface.knots_v.clone();
1770    let mut degree_u = surface.degree_u;
1771    let mut degree_v = surface.degree_v;
1772    let mut net = surface.control_points.clone();
1773    if in_v {
1774        if degree_v != 1 {
1775            return Ok(None);
1776        }
1777        let mut rows = Vec::with_capacity(net.len());
1778        for row in &net {
1779            let Some((knots, points)) = raise(&surface.knots_v, row) else {
1780                return Ok(None);
1781            };
1782            knots_v = knots;
1783            rows.push(points);
1784        }
1785        net = rows;
1786        degree_v = 3;
1787    }
1788    if in_u {
1789        if degree_u != 1 {
1790            return Ok(None);
1791        }
1792        let count_v = net[0].len();
1793        let mut columns = Vec::with_capacity(count_v);
1794        for column in 0..count_v {
1795            let row = net.iter().map(|row| row[column]).collect::<Vec<_>>();
1796            let Some((knots, points)) = raise(&surface.knots_u, &row) else {
1797                return Ok(None);
1798            };
1799            knots_u = knots;
1800            columns.push(points);
1801        }
1802        let count_u = columns[0].len();
1803        net = (0..count_u)
1804            .map(|row| columns.iter().map(|column| column[row]).collect())
1805            .collect();
1806        degree_u = 3;
1807    }
1808    Ok(Some(NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")?))
1809}
1810
1811/// `surface`, the same geometry with a knot inserted at the midpoint of every
1812/// non-empty span of each requested direction (exact Boehm insertion on the
1813/// homogeneous net, so the weights refine with it).
1814fn halve_knot_spans(surface: &NurbsSurface, in_u: bool, in_v: bool) -> Result<NurbsSurface, KernelRefusal> {
1815    fn midpoints(knots: &[f64], degree: usize) -> Vec<f64> {
1816        let last = knots.len() - 1 - degree;
1817        knots[degree..=last]
1818            .windows(2)
1819            .filter(|pair| pair[1] - pair[0] > 1e-12)
1820            .map(|pair| 0.5 * (pair[0] + pair[1]))
1821            .collect()
1822    }
1823    insert_offset_fit_knots(surface,
1824        &if in_u { midpoints(&surface.knots_u, surface.degree_u) } else { Vec::new() },
1825        &if in_v { midpoints(&surface.knots_v, surface.degree_v) } else { Vec::new() })
1826}
1827
1828/// Insert the fit's measured residual coordinates, rather than relocating
1829/// every collocation node by blindly halving all spans. In particular a
1830/// quadratic's first midpoint insertion loses its old central Greville node;
1831/// inserting the two residual lobes preserves that node and resolves them.
1832fn insert_offset_fit_knots(surface: &NurbsSurface, u: &[f64], v: &[f64]) -> Result<NurbsSurface, KernelRefusal> {
1833    let refine = |degree: usize, knots: &[f64], net: Vec<Vec<Vec4>>, inserted: &[f64]| -> Result<(Vec<f64>, Vec<Vec<Vec4>>), KernelRefusal> {
1834        let mut inserted = inserted.to_vec();
1835        inserted.sort_by(f64::total_cmp);
1836        inserted.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
1837        inserted.retain(|parameter| knots.iter().filter(|knot| (**knot - *parameter).abs() <= 1e-12).count() < degree);
1838        let mut refined_knots = knots.to_vec();
1839        let mut rows = Vec::with_capacity(net.len());
1840        for row in net {
1841            let mut curve = NurbsCurve::new(degree, knots.to_vec(), row).or_refuse(KernelStage::Refine, "new")?;
1842            for &parameter in &inserted {
1843                curve = curve.insert_knot(parameter, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
1844            }
1845            refined_knots = curve.knots.clone();
1846            rows.push(curve.control_points);
1847        }
1848        Ok((refined_knots, rows))
1849    };
1850    let mut knots_u = surface.knots_u.clone();
1851    let mut knots_v = surface.knots_v.clone();
1852    let mut net = surface.control_points.clone();
1853    if !v.is_empty() {
1854        let (knots, rows) = refine(surface.degree_v, &knots_v, net, v)?;
1855        knots_v = knots;
1856        net = rows;
1857    }
1858    if !u.is_empty() {
1859        // Transpose so each u column is a curve, refine, transpose back.
1860        let count_v = net[0].len();
1861        let columns = (0..count_v)
1862            .map(|column| net.iter().map(|row| row[column]).collect::<Vec<_>>())
1863            .collect::<Vec<_>>();
1864        let (knots, refined) = refine(surface.degree_u, &knots_u, columns, u)?;
1865        knots_u = knots;
1866        let count_u = refined[0].len();
1867        net = (0..count_u)
1868            .map(|row| refined.iter().map(|column| column[row]).collect())
1869            .collect();
1870    }
1871    NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
1872}
1873
1874/// A collocation fit of the pointwise offset, refined until its OUT-OF-SAMPLE
1875/// deviation meets the kernel's intersection-fit contract.
1876///
1877/// The Greville fit interpolates the pointwise offset in the SOURCE's basis.
1878/// That is exact wherever the offset lives in that basis — planes, cylinders,
1879/// cones, spheres, tori, which is why it was built that way — and, on a
1880/// free-form face, only as good as the source's span count: a single-span cubic
1881/// Bezier wall offset by 0.3 was measured 1.2e-2 to 1.7e-2 off its pointwise
1882/// offset, with the fit exact at its four Greville rows and nowhere else.
1883///
1884/// Refinement halves the knot spans of the direction the deviation is read in
1885/// (exact knot insertion, so the parameterization does not move: the same
1886/// `(u, v)` still names the offset of the same source point, and every trim
1887/// pcurve cloned onto the carrier stays valid) and re-fits. A fit already under
1888/// the bar is returned untouched, bit for bit. The bar is the one a carrier's
1889/// EDGES are already held to (`offset_face_carrier_impl`'s `fit_tolerance`),
1890/// derived from the source net's extent.
1891///
1892/// The contract is a TARGET; the offset construction band is the GATE. A fit
1893/// that is still over the contract when the rounds run out
1894/// ([`OFFSET_FIT_REFINEMENTS`]), when a round stops improving, or when the next
1895/// net would pass the solver ceilings, is ACCEPTED as its best measured round if
1896/// that round is inside `offset_construction_band` — the allowance every
1897/// offset construction in this family is already judged against — and refused
1898/// by name if it is not. Accepted over the contract is a decision, not a
1899/// silence: `offset_surface_measured` reports the deviation, and
1900/// `a_fit_the_rounds_leave_over_the_contract_is_accepted_inside_the_band` pins
1901/// it. (Measured on the corpus: two imported `OFFSET_SURFACE`s end their rounds
1902/// at 1.3e-5 and 3.4e-5 against a contract of 2.15e-6 and a band of 0.107.)
1903///
1904/// Cost is bounded by the rounds: a direction's spans grow at most 64-fold, a
1905/// round costs one pointwise offset per Greville node plus about four per node
1906/// to measure, and one dense collocation solve per row and per column
1907/// (`n_v·n_u³ + n_u·n_v³` for separable weights), and the rounds' costs are
1908/// geometric, so the last round is about half of the whole.
1909fn refine_offset_fit(
1910    source: &NurbsSurface,
1911    evaluator: &OffsetEvaluator,
1912    distance: f64,
1913    fitted: NurbsSurface,
1914) -> Result<NurbsSurface, KernelRefusal> {
1915    let scale = crate::model_scale(
1916        source
1917            .control_points
1918            .iter()
1919            .flatten()
1920            .filter_map(|point| point.point().ok()),
1921    );
1922    let bar = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
1923    let bar = if IMPORT_OFFSET_PRECISION.with(std::cell::Cell::get) { bar.min(1e-9) } else { bar };
1924    let trace = std::env::var("BREP_OFFSET_FIT_TRACE").ok();
1925    let started = web_time::Instant::now();
1926    let enabled = std::env::var("BREP_OFFSET_FIT_REFINE").as_deref() != Ok("0");
1927    let Ok(initial) = offset_fit_deviation(&fitted, evaluator, distance) else {
1928        return Ok(fitted);
1929    };
1930    let mut best = fitted;
1931    let mut best_deviation = initial.clone();
1932    let mut refined_source = source.clone();
1933    let mut rounds = 0usize;
1934    let mut residual_mode = false;
1935    let mut stop = "under bar";
1936    if enabled {
1937        while best_deviation.worst() > bar {
1938            if rounds == OFFSET_FIT_REFINEMENTS {
1939                stop = "refinement limit";
1940                break;
1941            }
1942            let (along_u, along_v) = (best_deviation.along_u > bar, best_deviation.along_v > bar);
1943            let (in_u, in_v) = if along_u || along_v {
1944                (along_u, along_v)
1945            } else {
1946                (true, true)
1947            };
1948            let (raise_u, raise_v) = (in_u && refined_source.degree_u == 1, in_v && refined_source.degree_v == 1);
1949            let raised = if raise_u || raise_v {
1950                raise_linear_directions(&refined_source, raise_u, raise_v)?
1951            } else {
1952                None
1953            };
1954            let (halve_u, halve_v) = match raised {
1955                Some(_) => (in_u && !raise_u, in_v && !raise_v),
1956                None => (in_u, in_v),
1957            };
1958            let raised = raised.unwrap_or_else(|| refined_source.clone());
1959            let candidate_source = if halve_u || halve_v {
1960                if residual_mode {
1961                    let (u, v) = best_deviation.refinement_sites(bar);
1962                    insert_offset_fit_knots(&raised,
1963                        if halve_u { &u } else { &[] }, if halve_v { &v } else { &[] })?
1964                } else {
1965                    halve_knot_spans(&raised, halve_u, halve_v)?
1966                }
1967            } else {
1968                raised
1969            };
1970            let weights = candidate_source
1971                .control_points
1972                .iter()
1973                .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1974                .collect::<Vec<_>>();
1975            let controls = weights.len() * weights[0].len();
1976            let ceiling = if separable_weights(&weights).is_some() {
1977                OFFSET_FIT_MAX_CONTROLS_SEPARABLE
1978            } else {
1979                OFFSET_FIT_MAX_CONTROLS_TENSOR
1980            };
1981            if controls > ceiling {
1982                stop = "net ceiling";
1983                break;
1984            }
1985            let knot_u = KnotVector::new(candidate_source.knots_u.clone(), candidate_source.degree_u).or_refuse(KernelStage::Refine, "new")?;
1986            let knot_v = KnotVector::new(candidate_source.knots_v.clone(), candidate_source.degree_v).or_refuse(KernelStage::Refine, "new")?;
1987            let parameters_u = greville_parameters(&knot_u);
1988            let parameters_v = greville_parameters(&knot_v);
1989            let samples = parameters_u
1990                .iter()
1991                .map(|&u| {
1992                    parameters_v
1993                        .iter()
1994                        .map(|&v| Ok(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1995                        .collect::<Result<Vec<_>, KernelRefusal>>()
1996                })
1997                .collect::<Result<Vec<_>, KernelRefusal>>();
1998            let Ok(samples) = samples else {
1999                stop = "unevaluable sample";
2000                break;
2001            };
2002            let candidate = NurbsSurface::new(
2003                candidate_source.degree_u,
2004                candidate_source.degree_v,
2005                candidate_source.knots_u.clone(),
2006                candidate_source.knots_v.clone(),
2007                interpolate_tensor(&knot_u, &knot_v, &parameters_u, &parameters_v, &samples, &weights)?,
2008            ).or_refuse(KernelStage::Refine, "new")?;
2009            let Ok(deviation) = offset_fit_deviation(&candidate, evaluator, distance) else {
2010                stop = "unmeasurable round";
2011                break;
2012            };
2013            rounds += 1;
2014            if deviation.worst() >= best_deviation.worst() {
2015                // One midpoint split of a quadratic Bezier adds no freedom
2016                // to its symmetric cross-section: the C1 halves still form
2017                // one symmetric quadratic. Its shifted interpolation nodes
2018                // can make this round worse even though the next subdivision
2019                // improves substantially. Try that next subdivision once,
2020                // keeping the previous fit until an improvement is measured.
2021                let first_quadratic_split = rounds == 1
2022                    && ((halve_u && source.degree_u == 2 && source.knots_u.len() == 6)
2023                        || (halve_v && source.degree_v == 2 && source.knots_v.len() == 6));
2024                if first_quadratic_split {
2025                    refined_source = candidate_source;
2026                    continue;
2027                }
2028                // Keep every improving uniform round on its existing path.
2029                // Only a measured stall switches the next attempt to the
2030                // residual locus, starting again from the best accepted net.
2031                // The rejected candidate never replaces that best fit.
2032                if !residual_mode && best_deviation.worst() > offset_construction_band(scale)
2033                    && std::env::var("BREP_OFFSET_FIT_RESIDUAL").as_deref() != Ok("0")
2034                {
2035                    residual_mode = true;
2036                    continue;
2037                }
2038                stop = "stopped improving";
2039                break;
2040            }
2041            best = candidate;
2042            best_deviation = deviation;
2043            refined_source = candidate_source;
2044        }
2045    } else if best_deviation.worst() > bar {
2046        stop = "refinement disabled";
2047    }
2048    // The gate. Refinement aims at the fit contract; a carrier it could not
2049    // bring inside the offset family's own acceptance band is refused by name
2050    // rather than handed on — a fit measured six orders past its band used to be
2051    // recorded and used anyway.
2052    let band = offset_construction_band(scale);
2053    let refused = enabled && best_deviation.worst() > band;
2054    if let Some(path) = trace {
2055        use std::io::Write;
2056        let caller = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
2057        let thread = std::thread::current().name().unwrap_or("-").to_string();
2058        let line = format!(
2059            "fit deg=({},{}) net=({},{})->({},{}) d={distance:.6} scale={scale:.4} bar={bar:.3e} \
2060             initial=(u {:.3e}, v {:.3e}, uv {:.3e}) final={:.3e} rounds={rounds} stop={stop} \
2061             refused={refused} seconds={:.4} thread={thread} args={caller}\n",
2062            source.degree_u,
2063            source.degree_v,
2064            source.control_points.len(),
2065            source.control_points[0].len(),
2066            best.control_points.len(),
2067            best.control_points[0].len(),
2068            initial.along_u,
2069            initial.along_v,
2070            initial.between,
2071            best_deviation.worst(),
2072            started.elapsed().as_secs_f64(),
2073        );
2074        if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
2075            let _ = file.write_all(line.as_bytes());
2076        }
2077    }
2078    if refused {
2079        return Err(KernelRefusal::non_convergence(KernelStage::Refine, "offset_carrier_fit", format!(
2080            "offset_surface: the fitted offset carrier stays {:.3e} off its pointwise offset at \
2081             distance {distance} after {rounds} refinement round(s) ({stop}), past the offset \
2082             construction band {band:.3e}",
2083            best_deviation.worst()
2084        )));
2085    }
2086    Ok(best)
2087}
2088
2089/// Two offset sheets of one source, re-expressed on ONE basis without moving
2090/// either: a linear direction facing a cubic one is raised, then each sheet
2091/// takes the knots the other has and it lacks (exact insertion).
2092///
2093/// [`refine_offset_fit`] refines each carrier by its own measured deviation, so
2094/// two sheets of the same source at different distances can leave on different
2095/// nets; a consumer that pairs their iso curves control point for control point
2096/// (`thicken`'s ruled walls) needs them on the same one. Weights follow the
2097/// insertions, and both sheets carry the source's weights refined the same way.
2098pub(crate) fn unify_offset_sheet_bases(
2099    first: &NurbsSurface,
2100    second: &NurbsSurface,
2101) -> Result<(NurbsSurface, NurbsSurface), KernelRefusal> {
2102    fn raise_to(surface: &NurbsSurface, degree_u: usize, degree_v: usize) -> Result<NurbsSurface, KernelRefusal> {
2103        let raise_u = surface.degree_u == 1 && degree_u == 3;
2104        let raise_v = surface.degree_v == 1 && degree_v == 3;
2105        if !raise_u && !raise_v {
2106            return Ok(surface.clone());
2107        }
2108        raise_linear_directions(surface, raise_u, raise_v)?
2109            .ok_or_else(|| {
2110                KernelRefusal::internal(
2111                    KernelStage::Refine,
2112                    "offset_raise_linear",
2113                    "offset sheets: a linear direction could not be raised",
2114                )
2115            })
2116    }
2117    let degree_u = first.degree_u.max(second.degree_u);
2118    let degree_v = first.degree_v.max(second.degree_v);
2119    let mut first = raise_to(first, degree_u, degree_v)?;
2120    let mut second = raise_to(second, degree_u, degree_v)?;
2121    if first.degree_u != second.degree_u || first.degree_v != second.degree_v {
2122        return Err(KernelRefusal::internal(KernelStage::Refine, "offset_common_basis", format!(
2123            "offset sheets: degrees ({},{}) and ({},{}) have no common basis",
2124            first.degree_u, first.degree_v, second.degree_u, second.degree_v
2125        )));
2126    }
2127    // Knots one vector has beyond the other, counted with multiplicity.
2128    fn missing(from: &[f64], into: &[f64]) -> Vec<f64> {
2129        let mut extra = Vec::new();
2130        let mut index = 0usize;
2131        for &knot in from {
2132            while index < into.len() && into[index] < knot - 1e-12 {
2133                index += 1;
2134            }
2135            if index < into.len() && (into[index] - knot).abs() <= 1e-12 {
2136                index += 1;
2137            } else {
2138                extra.push(knot);
2139            }
2140        }
2141        extra
2142    }
2143    fn insert(surface: &NurbsSurface, knots: &[f64], along_u: bool) -> Result<NurbsSurface, KernelRefusal> {
2144        if knots.is_empty() {
2145            return Ok(surface.clone());
2146        }
2147        let (degree, vector) = if along_u {
2148            (surface.degree_u, &surface.knots_u)
2149        } else {
2150            (surface.degree_v, &surface.knots_v)
2151        };
2152        let lines = if along_u {
2153            (0..surface.control_points[0].len())
2154                .map(|column| surface.control_points.iter().map(|row| row[column]).collect::<Vec<_>>())
2155                .collect::<Vec<_>>()
2156        } else {
2157            surface.control_points.clone()
2158        };
2159        let mut refined_knots = vector.clone();
2160        let mut refined = Vec::with_capacity(lines.len());
2161        for line in lines {
2162            let mut curve = NurbsCurve::new(degree, vector.clone(), line).or_refuse(KernelStage::Refine, "new")?;
2163            for &knot in knots {
2164                curve = curve.insert_knot(knot, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
2165            }
2166            refined_knots = curve.knots.clone();
2167            refined.push(curve.control_points);
2168        }
2169        let net = if along_u {
2170            (0..refined[0].len())
2171                .map(|row| refined.iter().map(|column| column[row]).collect())
2172                .collect()
2173        } else {
2174            refined
2175        };
2176        let (knots_u, knots_v) = if along_u {
2177            (refined_knots, surface.knots_v.clone())
2178        } else {
2179            (surface.knots_u.clone(), refined_knots)
2180        };
2181        NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
2182    }
2183    let (to_first_u, to_second_u) = (
2184        missing(&second.knots_u, &first.knots_u),
2185        missing(&first.knots_u, &second.knots_u),
2186    );
2187    first = insert(&first, &to_first_u, true)?;
2188    second = insert(&second, &to_second_u, true)?;
2189    let (to_first_v, to_second_v) = (
2190        missing(&second.knots_v, &first.knots_v),
2191        missing(&first.knots_v, &second.knots_v),
2192    );
2193    first = insert(&first, &to_first_v, false)?;
2194    second = insert(&second, &to_second_v, false)?;
2195    Ok((first, second))
2196}
2197
2198/// The 3D curve of one carrier edge: the image of the source coedge's pcurve
2199/// on the carrier surface, with the parameter range `(t0, t1)` that matches the
2200/// pcurve's domain fraction for fraction (the contract every coedge of this
2201/// carrier relies on, since the source pcurves are reused verbatim).
2202///
2203/// Built through the [`crate::image_curve`] ladder — exact on an affine sheet
2204/// (every planar offset), the exact iso-curve where the pcurve holds one
2205/// coordinate constant (a blend rail, a cylinder's cap rim, every
2206/// extrude/revolve boundary), and a fitted curve verified to `fit_tolerance`
2207/// off its nodes otherwise. Only where the ladder refuses does the edge fall
2208/// back to the degree-1 interpolant through `points` (the historical
2209/// construction), so nothing that offset before is refused now.
2210///
2211/// Why the polyline is no longer the first choice: its adaptive subdivision
2212/// stops at a 5e-4 chord sag, and a 128-chord rim on an r = 8.5 cylinder sits
2213/// up to 1.6e-4 inside the true arc. That is 16x the boolean imprint's
2214/// coincidence band, so when a later operation's section curve runs along that
2215/// rim (a cutter's side wall coplanar with the offset shell's opening wall,
2216/// 2026-09-06 report) the imprint cannot recognise the rim as the section it
2217/// already is, and reports every chord vertex as a crossing — one edge split
2218/// into forty. An exact arc is recognised as coincident and left alone.
2219fn carrier_edge_curve(
2220    surface: &NurbsSurface,
2221    pcurve: &NurbsCurve,
2222    points: &[Vec3],
2223    parameters: &[f64],
2224    fit_tolerance: f64,
2225) -> Result<(NurbsCurve, f64, f64), KernelRefusal> {
2226    // `BREP_CARRIER_POLYLINE=1` restores the polyline for an A/B comparison.
2227    // TRUSTED AFFINE IMAGE (lane O row 23, 2026-10-29;
2228    // on by default since the batch 6 promotion, `BREP_CARRIER_AFFINE_IMAGE=0` sweeps). On an affine carrier the image
2229    // is the pcurve's control net through the patch's own affine map, exact up to
2230    // round-off for any degree and weights, so `image_curve`'s 25-sample sweep
2231    // can only confirm it; this caller discards the deviation it measures. The
2232    // image and its range are what `image_curve`'s affine tier returns.
2233    if affine_image_trusted() && surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
2234        let [q0, q1] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2235        if (q1 - q0).is_finite() && (q1 - q0).abs() > 0.0 {
2236            let curve = crate::image_curve::affine_image_curve(surface, pcurve).or_refuse(KernelStage::Refine, "affine_image_curve")?;
2237            return Ok((curve, q0, q1));
2238        }
2239    }
2240    let image = if env_read!("BREP_CARRIER_POLYLINE").as_deref() == Some("1") {
2241        Err("BREP_CARRIER_POLYLINE set".to_string())
2242    } else {
2243        crate::image_curve::image_curve(surface, pcurve, fit_tolerance, "offset_face_carrier")
2244    };
2245    match image {
2246        Ok(image) if image.t0 <= image.t1 => Ok((image.curve, image.t0, image.t1)),
2247        Ok(image) => {
2248            // The image runs against the pcurve: reverse it so the edge range
2249            // is increasing, reflecting the range through the curve's domain.
2250            let [start, end] = image.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2251            let curve = image.curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
2252            Ok((curve, start + end - image.t0, start + end - image.t1))
2253        }
2254        Err(_) => {
2255            let curve = interpolate_curve(points, 1, parameters).or_refuse(KernelStage::Refine, "interpolate_curve")?;
2256            let [t0, t1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2257            Ok((curve, t0, t1))
2258        }
2259    }
2260}
2261
2262fn affine_image_trusted() -> bool {
2263    switch!("BREP_CARRIER_AFFINE_IMAGE", true)
2264}
2265
2266
2267/// Avoid constructing the fallback trace when the exact image will be used.
2268/// Positive, bounded control nets on well-separated knots make every skipped
2269/// in-domain evaluation total. The collapse witness is an actual vertex of
2270/// the old adaptive trace: its last point, or its midpoint when the first
2271/// subdivision fires. Near-collapsed images and uncertified inputs stay eager.
2272/// `BREP_OFFSET_LAZY_CARRIER_TRACE`; on by default since
2273/// 2026-11-02 (lane O batch 21), `0`/`off` restores the eager trace.
2274fn lazy_carrier_trace(
2275    surface: &NurbsSurface,
2276    pcurve: &NurbsCurve,
2277    collapse_tolerance: f64,
2278    fit_tolerance: f64,
2279) -> Option<(Vec<Vec3>, (NurbsCurve, f64, f64))> {
2280    if !switch!("BREP_OFFSET_LAZY_CARRIER_TRACE", true) {
2281        return None;
2282    }
2283    // A straight image costs only the initial five samples. Do not spend
2284    // certificate/ladder setup on those small traces.
2285    if pcurve.degree == 1 && pcurve.control_points.len() == 2 {
2286        let (a, b) = (pcurve.control_points[0], pcurve.control_points[1]);
2287        if (surface.degree_u <= 1 && surface.degree_v <= 1)
2288            || (a.w == b.w && a.y == b.y && surface.degree_u <= 1)
2289            || (a.w == b.w && a.x == b.x && surface.degree_v <= 1)
2290        {
2291            return None;
2292        }
2293    }
2294    fn knots_ok(knots: &[f64], degree: usize, count: usize) -> bool {
2295        degree <= 3 && count <= 64
2296            && KnotVector::new(knots.to_vec(), degree).ok().is_some_and(|k| k.control_point_count() == count)
2297            && knots.iter().all(|k| k.is_finite() && k.abs() <= 1e6)
2298            && knots.windows(2).all(|k| k[0] == k[1] || k[1] - k[0] >= 1e-6)
2299    }
2300    let bounded = |p: &Vec4| (1e-3..=1e3).contains(&p.w)
2301        && [p.x, p.y, p.z].iter().all(|x| x.is_finite() && x.abs() <= 1e12);
2302    let columns = surface.control_points.first()?.len();
2303    if !knots_ok(&surface.knots_u, surface.degree_u, surface.control_points.len())
2304        || !knots_ok(&surface.knots_v, surface.degree_v, columns)
2305        || surface.control_points.iter().any(|row| row.len() != columns || row.iter().any(|p| !bounded(p)))
2306        || !knots_ok(&pcurve.knots, pcurve.degree, pcurve.control_points.len())
2307        || pcurve.control_points.iter().any(|p| !bounded(p))
2308    {
2309        return None;
2310    }
2311    let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
2312    let uv = pcurve.control_points.iter().map(|p| p.point().ok()).collect::<Option<Vec<_>>>()?;
2313    if uv.iter().any(|p| p.x < u0 || p.x > u1 || p.y < v0 || p.y > v1) {
2314        return None;
2315    }
2316    // Only the ladder's exact tiers: a fitted image is not worth speculating.
2317    if !surface.is_affine().ok()? && !uv.iter().all(|p| p.x == uv[0].x) && !uv.iter().all(|p| p.y == uv[0].y) {
2318        return None;
2319    }
2320    let [start, end] = pcurve.domain().ok()?;
2321    let evaluate = |fraction: f64| {
2322        let uv = pcurve.evaluate(start + (end - start) * fraction).ok()?;
2323        surface.evaluate(uv.x, uv.y).ok()
2324    };
2325    let first = evaluate(0.0)?;
2326    let last = evaluate(1.0)?;
2327    let samples = [evaluate(0.25)?, evaluate(0.5)?, evaluate(0.75)?];
2328    let deviation = samples.iter().enumerate().map(|(index, point)| {
2329        point.sub(first.add(last.sub(first).scale((index + 1) as f64 * 0.25))).length()
2330    }).fold(0.0, f64::max);
2331    if last.sub(first).length() <= collapse_tolerance
2332        && !(deviation > 5e-4 && samples[1].sub(first).length() > collapse_tolerance)
2333    {
2334        return None;
2335    }
2336    // Empty fallback data deliberately declines a refused image; the caller
2337    // then takes the old trace and preserves its refusal and interpolation.
2338    let image = carrier_edge_curve(surface, pcurve, &[], &[], fit_tolerance).ok()?;
2339    Some((vec![first, samples[1], last], image))
2340}
2341
2342fn mapped_pcurve_polyline(
2343    surface: &NurbsSurface,
2344    pcurve: &NurbsCurve,
2345    degenerate: bool,
2346) -> Result<(Vec<Vec3>, Vec<f64>), KernelRefusal> {
2347    let [start, end] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2348    let evaluate = |fraction: f64| -> Result<Vec3, KernelRefusal> {
2349        let uv = pcurve.evaluate(start + (end - start) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
2350        surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")
2351    };
2352    let first = evaluate(0.0)?;
2353    let last = evaluate(1.0)?;
2354    if degenerate {
2355        return Ok((vec![first, last], vec![0.0, 1.0]));
2356    }
2357    fn append(
2358        evaluate: &impl Fn(f64) -> Result<Vec3, KernelRefusal>,
2359        a_fraction: f64,
2360        a: Vec3,
2361        b_fraction: f64,
2362        b: Vec3,
2363        depth: usize,
2364        parameters: &mut Vec<f64>,
2365        points: &mut Vec<Vec3>,
2366    ) -> Result<(), KernelRefusal> {
2367        let fractions =
2368            [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
2369        let samples = fractions
2370            .map(evaluate)
2371            .into_iter()
2372            .collect::<Result<Vec<_>, KernelRefusal>>()?;
2373        let deviation = samples
2374            .iter()
2375            .enumerate()
2376            .map(|(index, point)| {
2377                point
2378                    .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
2379                    .length()
2380            })
2381            .fold(0.0, f64::max);
2382        if deviation <= 5e-4 || depth >= 10 {
2383            parameters.push(b_fraction);
2384            points.push(b);
2385            return Ok(());
2386        }
2387        append(
2388            evaluate,
2389            a_fraction,
2390            a,
2391            fractions[1],
2392            samples[1],
2393            depth + 1,
2394            parameters,
2395            points,
2396        )?;
2397        append(
2398            evaluate,
2399            fractions[1],
2400            samples[1],
2401            b_fraction,
2402            b,
2403            depth + 1,
2404            parameters,
2405            points,
2406        )
2407    }
2408    let mut parameters = vec![0.0];
2409    let mut points = vec![first];
2410    append(
2411        &evaluate,
2412        0.0,
2413        first,
2414        1.0,
2415        last,
2416        0,
2417        &mut parameters,
2418        &mut points,
2419    )?;
2420    Ok((points, parameters))
2421}
2422
2423/// Everything an offset carrier's construction MEASURED about itself.
2424///
2425/// The half this kernel lacked: a tolerance MEASURED from the geometry that was
2426/// actually built, rather than a band chosen from `scale` before building. It
2427/// lives in the one place that needs no durable-format change: alongside the
2428/// transient construction result, never as a field on a [`crate::BrepSolid`]
2429/// record. `io/snapshot.rs` is a documented durable format and
2430/// `SOLID_CODEC_VERSION` a versioned wire layout; persisting per-entity
2431/// tolerances is real, planned, and separately designed. This lands the
2432/// measurement with no format churn at all.
2433///
2434/// Every number here is a RECORD. None of it widens a band — see
2435/// [`MeasuredTolerance`]'s direction rule.
2436#[derive(Clone, Debug)]
2437pub struct CarrierDeviation {
2438    /// The derived band every measurement below was judged against:
2439    /// [`crate::offset_construction_band`] of the source solid's extent.
2440    pub band: f64,
2441    /// Which branch built the carrier surface.
2442    pub lane: OffsetSurfaceLane,
2443    /// The carrier surface's own fit error, when the lane has one.
2444    pub surface: Option<MeasuredTolerance>,
2445    /// `max_t ‖C_3d(t) − S_off(p(t))‖` per carrier edge id, folded over every
2446    /// coedge that references the edge — OCCT's `FillEdgeData` rule
2447    /// (`BRepOffset_SimpleOffset.cxx:296-310`), which takes the maximum over
2448    /// **every** adjacent face rather than the first one.
2449    pub edges: Vec<(u64, MeasuredTolerance)>,
2450    /// Per carrier vertex id, propagated from the incident edge ends by
2451    /// [`crate::vertex_tolerance_from_edges`] — which is also where the verdict
2452    /// on OCCT's 1.001 inflation factor is recorded.
2453    pub vertices: Vec<(u64, f64)>,
2454}
2455
2456impl CarrierDeviation {
2457    /// The worst thing the construction did, against the tightest band it
2458    /// faced. `None` only when there was nothing at all to measure.
2459    pub fn worst(&self) -> Option<MeasuredTolerance> {
2460        MeasuredTolerance::worst(
2461            self.surface
2462                .into_iter()
2463                .chain(self.edges.iter().map(|(_, measured)| *measured))
2464                .chain(
2465                    self.vertices
2466                        .iter()
2467                        .map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
2468                ),
2469        )
2470    }
2471
2472    /// The entities whose measured deviation exceeded the derived band — the
2473    /// interesting case, and the only one any gate acts on.
2474    pub fn exceedances(&self) -> Vec<String> {
2475        let mut out = Vec::new();
2476        if let Some(surface) = self.surface {
2477            if surface.exceeds_band() {
2478                out.push(format!("carrier surface fit {}", surface.describe()));
2479            }
2480        }
2481        for (id, measured) in &self.edges {
2482            if measured.exceeds_band() {
2483                out.push(format!("edge {id} {}", measured.describe()));
2484            }
2485        }
2486        for (id, gap) in &self.vertices {
2487            let measured = MeasuredTolerance::new(*gap, self.band);
2488            if measured.exceeds_band() {
2489                out.push(format!("vertex {id} {}", measured.describe()));
2490            }
2491        }
2492        out
2493    }
2494}
2495
2496#[derive(Clone, Debug, Serialize)]
2497pub struct OffsetFaceCarrier {
2498    pub vertices: Vec<VertexRecord>,
2499    pub edges: Vec<EdgeRecord>,
2500    pub face: FaceRecord,
2501    /// What the construction measured about itself, or `None` when it was built
2502    /// through the unmeasured [`offset_face_carrier`] entry point.
2503    ///
2504    /// `#[serde(skip)]` on purpose: this struct crosses the wasm ABI as JSON
2505    /// (`abi/modeling_b.rs:500`), and a measurement is a diagnostic about a
2506    /// build, not part of the carrier the caller asked for. Skipping it keeps
2507    /// that payload byte-identical.
2508    #[serde(skip)]
2509    pub deviation: Option<CarrierDeviation>,
2510    /// Which branch built the carrier surface (always known; `deviation`
2511    /// carries it too, but only on the measured entry point). Skipped on the
2512    /// ABI for the same reason as `deviation`.
2513    #[serde(skip)]
2514    pub lane: OffsetSurfaceLane,
2515}
2516
2517/// The carrier vertex imaging `source_id` at `point`. A source vertex
2518/// normally has ONE image, claimed by the first edge to reach it. An APEX
2519/// does not: the offset of a corner blend's apex (a partial surface of
2520/// revolution about a reflex edge, its degenerate edge spanning the turn) is
2521/// an open ARC of radius |d|, and the two meridian edges into the apex end at
2522/// the arc's two ends. Handing every edge the first image collapsed that arc
2523/// onto one vertex while its curve still ran 1.7·d to the other end — a loop
2524/// that closes in ids and not in space, which the assembly could only leave
2525/// open (the 2026-10-18 shell report). An image farther than `split` from
2526/// every image already claimed is a vertex of its own.
2527fn claim_vertex_image(
2528    source_id: u64,
2529    point: Vec3,
2530    split: f64,
2531    vertex_images: &mut HashMap<u64, Vec<u64>>,
2532    vertices: &mut Vec<VertexRecord>,
2533    next_id: &mut u64,
2534) -> u64 {
2535    if let Some(ids) = vertex_images.get(&source_id) {
2536        let near = ids.iter().copied().find(|id| {
2537            vertices
2538                .iter()
2539                .find(|vertex| vertex.id == *id)
2540                .is_some_and(|vertex| vertex.point.sub(point).length() <= split)
2541        });
2542        if let Some(id) = near {
2543            return id;
2544        }
2545    }
2546    let id = *next_id;
2547    *next_id += 1;
2548    vertices.push(VertexRecord { id, point });
2549    vertex_images.entry(source_id).or_default().push(id);
2550    id
2551}
2552
2553pub fn offset_face_carrier(
2554    solid: &BrepSolid,
2555    face_id: u64,
2556    distance: f64,
2557    planar_extension: f64,
2558) -> Result<OffsetFaceCarrier, KernelRefusal> {
2559    offset_face_carrier_impl(
2560        solid,
2561        face_id,
2562        distance,
2563        &CarrierExtension::uniform(planar_extension),
2564        false,
2565    )
2566}
2567
2568/// [`offset_face_carrier`] with a per-side [`CarrierExtension`].
2569/// `BREP_OFFSET_PINCH_SIDE_BY_SOURCE` (default on; `=0` restores the size rule): which end of an inverted
2570/// ruled offset is past its pinch, read by direction rather than by size.
2571///
2572/// Cause (brep-bench probe, a cone r 3 h 4 shelled open at its base, walls 1.9
2573/// to 2.5 refused "unwelded rim leaves a non-watertight shell"): the pinch
2574/// retrim took the end with the SMALLER offset ring as the crossed one. Shrunk
2575/// by t, the base ring offsets to radius 3 − 0.8 t and the apex ring, crossed
2576/// through the axis, to 0.8 t; from t = 1.875 the true base ring is the smaller,
2577/// so the retrim pulled the cavity wall back to the pinch and kept the mirrored
2578/// sheet, which no weld can use. The crossed end is the one whose offset ring
2579/// points opposite to the source's own radial direction (read at the source's
2580/// wider ring, the other may be an apex). `None` when that reading does not
2581/// single out one end; the size rule stands.
2582fn pinch_side_by_source_enabled() -> bool {
2583    switch!("BREP_OFFSET_PINCH_SIDE_BY_SOURCE", true)
2584}
2585
2586fn pinch_side_by_source(
2587    source: &NurbsSurface,
2588    parameters_u: &[f64],
2589    parameters_v: &[f64],
2590    samples: &[Vec<Vec3>],
2591    near_centroid: Vec3,
2592    far_centroid: Vec3,
2593) -> Result<Option<bool>, KernelRefusal> {
2594    if !pinch_side_by_source_enabled() || parameters_v.len() != 2 {
2595        return Ok(None);
2596    }
2597    let mut source_rows = [Vec::new(), Vec::new()];
2598    for &u in parameters_u {
2599        for (end, &v) in parameters_v.iter().enumerate() {
2600            source_rows[end].push(source.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?);
2601        }
2602    }
2603    let centroid = |row: &[Vec3]| row.iter().fold(Vec3::default(), |sum, p| sum.add(*p)).scale(1.0 / row.len() as f64);
2604    let spread = |row: &[Vec3]| {
2605        let middle = centroid(row);
2606        row.iter().map(|p| p.sub(middle).length()).fold(0.0f64, f64::max)
2607    };
2608    let reference = if spread(&source_rows[0]) >= spread(&source_rows[1]) { 0 } else { 1 };
2609    let reference_centroid = centroid(&source_rows[reference]);
2610    let (mut near_sign, mut far_sign) = (0.0f64, 0.0f64);
2611    for (k, row) in samples.iter().enumerate() {
2612        let radial = source_rows[reference][k].sub(reference_centroid);
2613        near_sign += row[0].sub(near_centroid).dot(radial);
2614        far_sign += row[1].sub(far_centroid).dot(radial);
2615    }
2616    Ok(match (near_sign > 0.0, far_sign > 0.0) {
2617        (true, false) => Some(true),
2618        (false, true) => Some(false),
2619        _ => None,
2620    })
2621}
2622
2623pub fn offset_face_carrier_sided(
2624    solid: &BrepSolid,
2625    face_id: u64,
2626    distance: f64,
2627    extension: &CarrierExtension,
2628) -> Result<OffsetFaceCarrier, KernelRefusal> {
2629    offset_face_carrier_impl(solid, face_id, distance, extension, false)
2630}
2631
2632/// [`offset_face_carrier`], with every entity it builds measured against the
2633/// deviation it was meant to reproduce.
2634///
2635/// The carrier is bit-identical to [`offset_face_carrier`]'s — same body, same
2636/// arithmetic, in the same order — with a read-only measurement pass appended.
2637/// Three quantities land in [`CarrierDeviation`]:
2638///
2639/// * the carrier SURFACE's fit against the pointwise offset it interpolates
2640///   ([`offset_surface_measured`]);
2641/// * each carrier EDGE's 3D curve against the locus its pcurve traces on that
2642///   surface — the trim boundary is built by sampling exactly that composition
2643///   and interpolating the images, so this is the construction's own claim,
2644///   measured rather than assumed;
2645/// * each carrier VERTEX, propagated from the incident edge ends.
2646///
2647/// The interesting output is [`CarrierDeviation::exceedances`]: an entity whose
2648/// measured deviation is worse than the size-derived band assumed. That is a
2649/// construction that went wrong in a way the derived band alone cannot see, and
2650/// it is the case a caller should refuse on rather than ship.
2651pub fn offset_face_carrier_measured(
2652    solid: &BrepSolid,
2653    face_id: u64,
2654    distance: f64,
2655    planar_extension: f64,
2656) -> Result<OffsetFaceCarrier, KernelRefusal> {
2657    offset_face_carrier_impl(
2658        solid,
2659        face_id,
2660        distance,
2661        &CarrierExtension::uniform(planar_extension),
2662        true,
2663    )
2664}
2665
2666/// The measured deviations of one built carrier: surface fit, per edge, per
2667/// vertex.
2668///
2669/// Read-only over what the construction produced. The edge measurement is
2670/// [`crate::measure_edge_against_pcurve_image`] — validate's own
2671/// `adaptive_coedge_error` floored by a span-midpoint pass, because the sampler
2672/// alone is aliased against exactly the curves this construction builds (see
2673/// that module's doc) — taken against the far tighter
2674/// [`crate::offset_construction_band`] instead of the vendor-forgiving
2675/// `pcurve_acceptance` validate will use later.
2676fn measure_carrier(
2677    surface: &NurbsSurface,
2678    vertices: &[VertexRecord],
2679    edges: &[EdgeRecord],
2680    loops: &[LoopRecord],
2681    lane: OffsetSurfaceLane,
2682    surface_fit: Option<MeasuredTolerance>,
2683    band: f64,
2684) -> Result<CarrierDeviation, KernelRefusal> {
2685    let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
2686    // Fold over coedges, not edges: a seam edge is referenced twice with two
2687    // different pcurves, and OCCT's `FillEdgeData` takes the maximum over every
2688    // adjacent face for exactly that reason.
2689    let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
2690    for loop_record in loops {
2691        for coedge in &loop_record.coedges {
2692            let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
2693                continue;
2694            };
2695            let measured = measure_edge_against_pcurve_image(
2696                surface,
2697                &coedge.pcurve,
2698                edge,
2699                coedge.forward,
2700                band,
2701            )?;
2702            per_edge
2703                .entry(edge.id)
2704                .and_modify(|existing| *existing = existing.worse_of(measured))
2705                .or_insert(measured);
2706        }
2707    }
2708
2709    let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
2710    measured_edges.sort_by_key(|(id, _)| *id);
2711    let deviation_of: HashMap<u64, f64> = measured_edges
2712        .iter()
2713        .map(|(id, measured)| (*id, measured.deviation()))
2714        .collect();
2715
2716    // Edge ENDS by the vertex they claim, built once. A degenerate edge claims
2717    // the same vertex at both ends and contributes both, which is right: the
2718    // question is how far every representation meeting there actually lands.
2719    let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
2720    for edge in edges {
2721        ends_at
2722            .entry(edge.start_vertex_id)
2723            .or_default()
2724            .push((edge, edge.t0));
2725        ends_at
2726            .entry(edge.end_vertex_id)
2727            .or_default()
2728            .push((edge, edge.t1));
2729    }
2730
2731    let mut measured_vertices = Vec::with_capacity(vertices.len());
2732    for vertex in vertices {
2733        let mut gaps = Vec::new();
2734        let mut incident = Vec::new();
2735        for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
2736            gaps.push(vertex_endpoint_gap(
2737                vertex.point,
2738                edge.curve.evaluate(*parameter).or_refuse(KernelStage::Refine, "evaluate")?,
2739            ));
2740            incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
2741        }
2742        measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
2743    }
2744
2745    Ok(CarrierDeviation {
2746        band,
2747        lane,
2748        surface: surface_fit,
2749        edges: measured_edges,
2750        vertices: measured_vertices,
2751    })
2752}
2753
2754fn offset_face_carrier_impl(
2755    solid: &BrepSolid,
2756    face_id: u64,
2757    distance: f64,
2758    extension: &CarrierExtension,
2759    measure: bool,
2760) -> Result<OffsetFaceCarrier, KernelRefusal> {
2761    let source = solid
2762        .shells
2763        .iter()
2764        .flat_map(|shell| &shell.faces)
2765        .find(|face| face.id == face_id)
2766        .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}")).or_refuse(KernelStage::Refine, "offset_missing_face")?;
2767    // The band is derived from the SOURCE SOLID's extent, matching every other
2768    // direct-edit/offset site (`face_offset.rs:103` and its siblings all take
2769    // `solid_model_scale`). The alternative basis is the FACE's own extent — the
2770    // reference shell keys its size-relative offset quantities on the 3D arc
2771    // length of the face's own boundary iso, never on the assembly. Which basis
2772    // is better is unsettled: it is a measurement to make over the refusal
2773    // corpus before switching, not a change to smuggle in here.
2774    let band = offset_construction_band(solid_model_scale(solid));
2775    // The accuracy bar a carrier EDGE's 3D curve must meet against the locus its
2776    // pcurve traces on the carrier surface — the kernel's own SSI fit contract,
2777    // the same bar `thicken` holds its wall boundaries to. NOT the construction
2778    // band: that is the surface-fit allowance (5e-4 of the part), 50x looser
2779    // than the boolean's coincidence band, and an edge fitted only that well is
2780    // what a later imprint mistakes for a crossing (see `carrier_edge_curve`).
2781    let fit_tolerance =
2782        crate::KernelTolerances::for_scale(solid_model_scale(solid), 1e-7).intersection_fit;
2783    let (surface, lane, surface_fit) = if measure {
2784        let measured = offset_surface_measured_sided(source, distance, extension, band)?;
2785        (measured.surface, measured.lane, measured.fit)
2786    } else {
2787        let (surface, lane) = offset_surface_with_lane(source, distance, extension)?;
2788        (surface, lane, None)
2789    };
2790    let source_edges = solid
2791        .edges
2792        .iter()
2793        .map(|edge| (edge.id, edge))
2794        .collect::<HashMap<_, _>>();
2795    let source_vertices = solid
2796        .vertices
2797        .iter()
2798        .map(|vertex| (vertex.id, vertex))
2799        .collect::<HashMap<_, _>>();
2800    let mut vertices = Vec::new();
2801    let mut vertex_images = HashMap::default();
2802    // Two images of one source vertex this far apart are two vertices (an
2803    // apex's arc, `claim_vertex_image`); fitting noise at a pole stays far
2804    // below it.
2805    let vertex_split = (distance.abs() * 0.1).max(band);
2806    let mut edges = Vec::new();
2807    let mut edge_images = HashMap::default();
2808    let mut loops = Vec::new();
2809    let mut next_id = 1u64;
2810
2811    for source_loop in &source.loops {
2812        let mut coedges = Vec::new();
2813        for source_coedge in &source_loop.coedges {
2814            let source_edge = source_edges
2815                .get(&source_coedge.edge_id)
2816                .ok_or_else(|| "offset_face_carrier: missing source edge".to_string()).or_refuse(KernelStage::Refine, "offset_missing_edge")?;
2817            let (source_start, source_end) = if source_coedge.forward {
2818                (source_edge.start_vertex_id, source_edge.end_vertex_id)
2819            } else {
2820                (source_edge.end_vertex_id, source_edge.start_vertex_id)
2821            };
2822            if !source_vertices.contains_key(&source_start)
2823                || !source_vertices.contains_key(&source_end)
2824            {
2825                return Err(KernelRefusal::internal(KernelStage::Refine, "offset_missing_vertex", "offset_face_carrier: missing source vertex"));
2826            }
2827            // Map even DEGENERATE source edges through the full polyline: a
2828            // cone apex's image on the offset surface is a genuine CIRCLE
2829            // (radius d·cos half-angle), not a point — shortcutting to the
2830            // two endpoints would collapse the ring and leave the carrier's
2831            // topology inconsistent with its surface. Edges whose image truly
2832            // collapses (sphere poles, planar corners) still interpolate to a
2833            // point-sized curve and keep their degenerate flag below.
2834            // Map even DEGENERATE source edges through the full polyline: an
2835            // EXTERIOR cone offset turns the apex point into a genuine RING
2836            // (radius d·cos half-angle) — shortcutting to the endpoints would
2837            // collapse it and leave the carrier topology inconsistent with
2838            // its surface (and the ring imprint would be dropped as
2839            // boundary-coincident with a "degenerate" edge). Images that
2840            // truly collapse (sphere poles; interior apexes after the pinch
2841            // retrim) stay degenerate below. The threshold scales with the
2842            // offset distance: a real ring measures ~d·cos α, while fitted
2843            // pole rows wobble ~1e-4 absolute.
2844            let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
2845            let (points, parameters, exact_image) = match lazy_carrier_trace(
2846                &surface, &source_coedge.pcurve, collapse_tolerance, fit_tolerance,
2847            ) {
2848                Some((points, image)) => (points, Vec::new(), Some(image)),
2849                None => {
2850                    let (points, parameters) = mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
2851                    (points, parameters, None)
2852                }
2853            };
2854            let image_collapsed = points
2855                .iter()
2856                .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
2857            let (edge_id, forward) =
2858                if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
2859                    edge_images.get(&source_edge.id)
2860                {
2861                    if source_start == source_end {
2862                        // A CLOSED source edge (a seam of a periodic face: both
2863                        // ends are the same vertex) is referenced twice by the
2864                        // same loop, once per seam side, and the two references
2865                        // traverse it in OPPOSITE senses — that is what closes
2866                        // the loop. The endpoint test below cannot see that:
2867                        // both ends map to the same vertex image, so it answers
2868                        // `true` for both references and the returning coedge
2869                        // comes back mis-oriented.
2870                        //
2871                        // Measured on a full torus (one vertex, two seam edges):
2872                        // the source records `forward: false` on the two
2873                        // returning coedges and validates clean, while the
2874                        // carrier recorded `forward: true` on both and its rim
2875                        // edge measured 12.5 — a whole part diameter — against
2876                        // its own composed pcurve image. The measured tolerance
2877                        // this slice adds is what surfaced it; no existing gate
2878                        // reaches this shape.
2879                        //
2880                        // The source coedge's own sense is the answer: the image
2881                        // edge runs along the CREATING coedge's pcurve, so a
2882                        // later reference runs with it exactly when the two
2883                        // source coedges traverse the source edge the same way.
2884                        (*edge_id, source_coedge.forward == *creator_forward)
2885                    } else {
2886                        // UNCHANGED for every open edge: the image edge's start
2887                        // vertex identifies which way this coedge runs.
2888                        (
2889                            *edge_id,
2890                            vertex_images
2891                                .get(&source_start)
2892                                .is_some_and(|ids: &Vec<u64>| ids.contains(edge_start_vertex_id)),
2893                        )
2894                    }
2895                } else {
2896                    let (curve, t0, t1) = if image_collapsed {
2897                        let curve = NurbsCurve::new(
2898                            1,
2899                            vec![0.0, 0.0, 1.0, 1.0],
2900                            vec![
2901                                Vec4::from_point(points[0], 1.0),
2902                                Vec4::from_point(points[0], 1.0),
2903                            ],
2904                        ).or_refuse(KernelStage::Refine, "new")?;
2905                        (curve, 0.0, 1.0)
2906                    } else if let Some(image) = exact_image {
2907                        image
2908                    } else {
2909                        carrier_edge_curve(
2910                            &surface,
2911                            &source_coedge.pcurve,
2912                            &points,
2913                            &parameters,
2914                            fit_tolerance,
2915                        )?
2916                    };
2917                    let start_vertex_id = claim_vertex_image(
2918                        source_start,
2919                        points[0],
2920                        vertex_split,
2921                        &mut vertex_images,
2922                        &mut vertices,
2923                        &mut next_id,
2924                    );
2925                    let end_vertex_id = claim_vertex_image(
2926                        source_end,
2927                        points[points.len() - 1],
2928                        vertex_split,
2929                        &mut vertex_images,
2930                        &mut vertices,
2931                        &mut next_id,
2932                    );
2933                    let id = next_id;
2934                    next_id += 1;
2935                    edges.push(EdgeRecord {
2936                        id,
2937                        curve,
2938                        t0,
2939                        t1,
2940                        start_vertex_id,
2941                        end_vertex_id,
2942                        // Degenerate exactly when the IMAGE collapsed. A cone
2943                        // apex maps to a real RING on an exterior offset and
2944                        // must carry a real closed edge; a real source edge
2945                        // whose image collapses to a point is a degenerate edge
2946                        // ON THE CARRIER whatever it was on the source.
2947                        //
2948                        // That second half is what the CARVE needs. The new
2949                        // boundary a carve introduces is a piece of the fold
2950                        // locus, which on the source is an ordinary curve — a
2951                        // parallel circle on a cone — and whose whole defining
2952                        // property is that the offset collapses there. Reading
2953                        // the source's flag as well left it a zero-length
2954                        // NON-degenerate closed edge, which no weld can use.
2955                        degenerate: image_collapsed,
2956                        // Image of a named source edge on the offset carrier;
2957                        // suffixed so it cannot collide with the source edge
2958                        // when both faces survive into one solid.
2959                        name: source_edge
2960                            .name
2961                            .as_ref()
2962                            .map(|name| format!("{name}_Offset")),
2963                    });
2964                    edge_images.insert(
2965                        source_edge.id,
2966                        (id, start_vertex_id, source_coedge.forward),
2967                    );
2968                    (id, true)
2969                };
2970            let id = next_id;
2971            next_id += 1;
2972            coedges.push(CoedgeRecord {
2973                id,
2974                edge_id,
2975                forward,
2976                pcurve: source_coedge.pcurve.clone(),
2977            });
2978        }
2979        let id = next_id;
2980        next_id += 1;
2981        loops.push(LoopRecord { id, coedges });
2982    }
2983    let deviation = if measure {
2984        Some(measure_carrier(
2985            &surface,
2986            &vertices,
2987            &edges,
2988            &loops,
2989            lane,
2990            surface_fit,
2991            band,
2992        )?)
2993    } else {
2994        None
2995    };
2996    Ok(OffsetFaceCarrier {
2997        vertices,
2998        edges,
2999        face: FaceRecord {
3000            id: next_id,
3001            surface,
3002            same_sense: source.same_sense,
3003            loops,
3004            name: source.name.as_ref().map(|name| format!("{name}_Offset")),
3005        },
3006        deviation,
3007        lane,
3008    })
3009}
3010
3011
3012
3013
3014