Skip to main content

brep_kernel/offset/
offset.rs

1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use crate::fit::solve_dense;
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        let mut intermediate = vec![vec![Vec3::default(); count_v]; count_u];
118        for column in 0..count_v {
119            let solve_axis = |axis: fn(Vec3) -> f64| {
120                solve_dense(
121                    matrix_u.clone(),
122                    samples.iter().map(|row| axis(row[column])).collect(),
123                )
124            };
125            let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
126            let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
127            let z = solve_axis(|point| point.z).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 solve_axis = |axis: fn(Vec3) -> f64| {
135                solve_dense(
136                    matrix_v.clone(),
137                    intermediate[row].iter().copied().map(axis).collect(),
138                )
139            };
140            let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
141            let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
142            let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
143            for column in 0..count_v {
144                controls[row][column] = Vec4::from_point(
145                    Vec3::new(x[column], y[column], z[column]),
146                    weights[row][column],
147                );
148            }
149        }
150        return Ok(controls);
151    }
152
153    // Non-separable weights: solve the full tensor collocation system with
154    // the exact 2D rational basis. Nets are small in practice.
155    let unknowns = count_u * count_v;
156    let mut matrix = vec![vec![0.0; unknowns]; unknowns];
157    for (k, &u) in parameters_u.iter().enumerate() {
158        let span_u = knot_u.find_span(u);
159        let basis_u = knot_u.basis_functions(span_u, u);
160        for (l, &v) in parameters_v.iter().enumerate() {
161            let span_v = knot_v.find_span(v);
162            let basis_v = knot_v.basis_functions(span_v, v);
163            let row = &mut matrix[k * count_v + l];
164            let mut denominator = 0.0;
165            for (du, value_u) in basis_u.iter().enumerate() {
166                let i = span_u - knot_u.degree + du;
167                for (dv, value_v) in basis_v.iter().enumerate() {
168                    let j = span_v - knot_v.degree + dv;
169                    let entry = value_u * value_v * weights[i][j];
170                    row[i * count_v + j] = entry;
171                    denominator += entry;
172                }
173            }
174            if denominator.abs() > 0.0 {
175                for value in row.iter_mut() {
176                    *value /= denominator;
177                }
178            }
179        }
180    }
181    let solve_axis = |axis: fn(Vec3) -> f64| {
182        solve_dense(
183            matrix.clone(),
184            samples
185                .iter()
186                .flat_map(|row| row.iter().copied().map(axis))
187                .collect(),
188        )
189    };
190    let x = solve_axis(|point| point.x).or_refuse(KernelStage::Refine, "solve_axis")?;
191    let y = solve_axis(|point| point.y).or_refuse(KernelStage::Refine, "solve_axis")?;
192    let z = solve_axis(|point| point.z).or_refuse(KernelStage::Refine, "solve_axis")?;
193    let mut controls = vec![vec![Vec4::from_point(Vec3::default(), 1.0); count_v]; count_u];
194    for row in 0..count_u {
195        for column in 0..count_v {
196            let index = row * count_v + column;
197            controls[row][column] = Vec4::from_point(
198                Vec3::new(x[index], y[index], z[index]),
199                weights[row][column],
200            );
201        }
202    }
203    Ok(controls)
204}
205
206/// Which branch of [`offset_surface`] built a carrier — and, with it, whether
207/// comparing that carrier against the pointwise offset at the same `(u, v)` is
208/// even the right question.
209///
210/// Recorded rather than inferred, in the pattern `offset/reintersect.rs`
211/// established for its own two lanes: two of this function's three branches
212/// deliberately move the result off the pointwise offset, and a measurement that
213/// did not know which branch ran would report a designed divergence as a fit
214/// error.
215#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
216pub enum OffsetSurfaceLane {
217    /// A rigid control-net shift of an affine (plane-like) carrier. The normal
218    /// is constant, so the shifted net IS the pointwise offset — exact by
219    /// construction, with no fit to measure.
220    Affine,
221    /// A CLOSED-FORM offset built from the source's recognized analytic shape
222    /// rather than sampled: a sphere's concentric mate, whose net is the
223    /// source's scaled about the centre. Exact like `Affine`, but derived from
224    /// geometry the recognizer proved rather than from a constant normal, so
225    /// `offset_surface_measured_sided` measures it instead of declaring it.
226    Analytic,
227    /// Greville collocation of the pointwise offset. `S_fit(u, v)` is meant to
228    /// BE `offset(u, v)`, and the distance between them is the fit error this
229    /// slice measures.
230    #[default]
231    Fit,
232    /// The result was deliberately moved off the pointwise offset: the planar /
233    /// ruled EXTENSION (which grows the carrier past its source rim, so the same
234    /// `(u, v)` names a different point) or the apex-cone PINCH RETRIM (which
235    /// pulls the crossed sample row back to the offset cone's own apex). Both
236    /// are correct and intended; neither is comparable pointwise.
237    Reparameterised,
238}
239
240/// A fitted offset carrier together with what its construction measured about
241/// itself.
242#[derive(Clone, Debug)]
243pub struct MeasuredOffsetSurface {
244    pub surface: NurbsSurface,
245    pub lane: OffsetSurfaceLane,
246    /// `max ‖S_fit(u, v) − offset(u, v)‖` over
247    /// [`crate::measure_surface_fit_against_pointwise_offset`]'s grid, against
248    /// the band it was judged with. When the RULED EXTENSION fired, the offset
249    /// compared against is the offset of the source EXTENDED ALONG ITS RULINGS —
250    /// the surface the carrier is the pointwise offset of — not of `face.surface`.
251    ///
252    /// `None` for [`OffsetSurfaceLane::Reparameterised`] — see that variant.
253    pub fit: Option<MeasuredTolerance>,
254}
255
256/// How far an offset carrier's TRIM grows past the source face at each of
257/// its four parametric sides, in world units.
258///
259/// A carrier is trimmed by the parametric IMAGE of the source loops, so the
260/// growth is realised by reshaping the carrier surface's net while the cloned
261/// pcurves stay put: an affine plane's 2x2 net slides along the plane, a ruled
262/// (linear-v) net stretches each sampled ruling. Both are solved so that the
263/// trim's own uv bounding box moves by exactly these amounts — a trim that
264/// occupies a sub-range of its surface's domain (every booleaned or
265/// blend-trimmed face) grows by the same distance as one that spans it. The
266/// legacy `planar_extension: f64` entry points are the uniform case.
267#[derive(Clone, Copy, Debug, PartialEq)]
268pub struct CarrierExtension {
269    pub u_min: f64,
270    pub u_max: f64,
271    pub v_min: f64,
272    pub v_max: f64,
273}
274
275impl CarrierExtension {
276    pub const NONE: CarrierExtension = CarrierExtension {
277        u_min: 0.0,
278        u_max: 0.0,
279        v_min: 0.0,
280        v_max: 0.0,
281    };
282
283    pub fn uniform(amount: f64) -> CarrierExtension {
284        CarrierExtension {
285            u_min: amount,
286            u_max: amount,
287            v_min: amount,
288            v_max: amount,
289        }
290    }
291
292    pub fn is_active(&self) -> bool {
293        self.u_min > 0.0 || self.u_max > 0.0 || self.v_min > 0.0 || self.v_max > 0.0
294    }
295
296    fn extends_v(&self) -> bool {
297        self.v_min > 0.0 || self.v_max > 0.0
298    }
299}
300
301/// The trim's uv bounding box as FRACTIONS of the surface domain:
302/// `([ta_u, tb_u], [ta_v, tb_v])`, each in `[0, 1]`. Sampled over every
303/// coedge pcurve of every loop.
304fn trim_domain_fractions(
305    face: &FaceRecord,
306    [u0, u1]: [f64; 2],
307    [v0, v1]: [f64; 2],
308) -> Result<([f64; 2], [f64; 2]), KernelRefusal> {
309    let mut low = Vec2 {
310        x: f64::INFINITY,
311        y: f64::INFINITY,
312    };
313    let mut high = Vec2 {
314        x: f64::NEG_INFINITY,
315        y: f64::NEG_INFINITY,
316    };
317    for coedge in face
318        .loops
319        .iter()
320        .flat_map(|loop_record| &loop_record.coedges)
321    {
322        let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
323        for sample in 0..=24 {
324            let uv = coedge
325                .pcurve
326                .evaluate(p0 + (p1 - p0) * sample as f64 / 24.0).or_refuse(KernelStage::Refine, "evaluate")?;
327            low.x = low.x.min(uv.x);
328            low.y = low.y.min(uv.y);
329            high.x = high.x.max(uv.x);
330            high.y = high.y.max(uv.y);
331        }
332    }
333    let fraction = |value: f64, start: f64, end: f64| {
334        let span = end - start;
335        if span.abs() <= f64::EPSILON || !value.is_finite() {
336            return None;
337        }
338        Some(((value - start) / span).clamp(0.0, 1.0))
339    };
340    let u = match (fraction(low.x, u0, u1), fraction(high.x, u0, u1)) {
341        (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
342        _ => [0.0, 1.0],
343    };
344    let v = match (fraction(low.y, v0, v1), fraction(high.y, v0, v1)) {
345        (Some(a), Some(b)) if b - a > 1e-6 => [a, b],
346        _ => [0.0, 1.0],
347    };
348    Ok((u, v))
349}
350
351/// How far the surface's domain ENDS must move (`(back, forward)`: the start
352/// end backwards, the far end forwards, world units) so that a trim occupying
353/// the domain fractions `[ta, tb]` grows by exactly `grow_min` at its start and
354/// `grow_max` at its end when its pcurves are left untouched. A point at
355/// fraction `t` of a linearly re-mapped domain moves by
356/// `-(1 - t)·back + t·forward`; solving that at `ta` and `tb` gives the pair
357/// below. It reduces to `(grow_min, grow_max)` for a full-domain trim, and
358/// never shrinks either end (both results are non-negative for non-negative
359/// inputs).
360fn domain_end_moves(grow_min: f64, grow_max: f64, [ta, tb]: [f64; 2]) -> (f64, f64) {
361    let span = tb - ta;
362    if span <= 1e-6 {
363        return (grow_min, grow_max);
364    }
365    let rate = (grow_min + grow_max) / span;
366    let back = grow_min + ta * rate;
367    let forward = (1.0 - ta) * rate - grow_min;
368    (back.max(0.0), forward.max(0.0))
369}
370
371/// Construct the same fitted offset carrier surface as the reference shell
372/// implementation. Positive distance follows its convention and moves
373/// opposite the face's outward normal.
374pub fn offset_surface(
375    face: &FaceRecord,
376    distance: f64,
377    planar_extension: f64,
378) -> Result<NurbsSurface, KernelRefusal> {
379    offset_surface_with_lane(face, distance, &CarrierExtension::uniform(planar_extension))
380        .map(|(surface, _)| surface)
381}
382
383/// [`offset_surface`], plus the deviation MEASURED between the carrier it built
384/// and the pointwise offset that carrier approximates.
385///
386/// The surface is bit-identical to [`offset_surface`]'s — this calls the same
387/// body and adds a read-only pass afterwards. Nothing here can change the
388/// carrier: measuring the deviation AFTER the carrier is built records what
389/// happened; the size-derived band stays the floor and the bar it is judged
390/// against. A measurement never widens a band.
391pub fn offset_surface_measured(
392    face: &FaceRecord,
393    distance: f64,
394    planar_extension: f64,
395    band: f64,
396) -> Result<MeasuredOffsetSurface, KernelRefusal> {
397    offset_surface_measured_sided(
398        face,
399        distance,
400        &CarrierExtension::uniform(planar_extension),
401        band,
402    )
403}
404
405/// [`offset_surface_measured`] with a per-side [`CarrierExtension`].
406pub fn offset_surface_measured_sided(
407    face: &FaceRecord,
408    distance: f64,
409    extension: &CarrierExtension,
410    band: f64,
411) -> Result<MeasuredOffsetSurface, KernelRefusal> {
412    let (surface, lane, extended) = offset_surface_constructed(face, distance, extension)?;
413    let fit = match lane {
414        OffsetSurfaceLane::Affine => Some(MeasuredTolerance::exact(band)),
415        // `Analytic` is exact by construction and already verified against the
416        // pointwise offset before it was returned — but it is measured here
417        // like a fit anyway, so the record carries the number instead of the
418        // claim. It reads ~1e-13 where `Fit` reads ~1e-6.
419        OffsetSurfaceLane::Analytic | OffsetSurfaceLane::Fit => {
420            Some(measure_surface_fit_against_pointwise_offset(
421                extended.as_ref().unwrap_or(&face.surface),
422                face.same_sense,
423                &surface,
424                distance,
425                band,
426            )?)
427        }
428        OffsetSurfaceLane::Reparameterised => None,
429    };
430    Ok(MeasuredOffsetSurface { surface, lane, fit })
431}
432
433/// The EXACT concentric offset of a sphere, or `None` for every other carrier.
434///
435/// A sphere's normal is radial, so its pointwise offset at distance `d` is the
436/// concentric sphere of radius `R' = R ± d`. A NURBS map is affine in its
437/// control points — `S = Σ N_i w_i P_i / Σ N_i w_i` — so scaling every control
438/// point about the centre by `R'/R` scales the whole image by that factor about
439/// that point, and the result IS that sphere to rounding. Knots, degrees and
440/// weights are untouched, so the same `(u, v)` still names the corresponding
441/// point: the trims `offset_face_carrier_impl` clones onto this carrier
442/// verbatim land exactly where they did on the source.
443///
444/// The Greville fit below reaches the same surface only to its collocation
445/// error. On `offset-shell-box-bore-dished-face` that error put the dished
446/// face's volume contribution 4.69e-3 away from the closed form — the larger
447/// share of the case's 3.9435e-3 deficit.
448///
449/// Everything here is COMPUTED, never claimed. The offset radius comes from the
450/// same [`OffsetEvaluator`] the fit lane samples, so this lane cannot invent a
451/// sign convention; the guards below reject a centre the recognizer got wrong
452/// or a sphere turned inside out by an inward offset deeper than its radius;
453/// and the finished net is checked against that evaluator on a grid before it
454/// is returned. Every refusal falls through to the fit, which is what built
455/// these carriers before.
456///
457/// A sphere the recognizer does not recognize — a FITTED patch that merely
458/// looks spherical — stays on the fit lane, unchanged. This lane widens nothing
459/// on its own: cylinders, cones and tori want a scaling about an axis or a
460/// spine rather than a point, which is a different construction.
461fn sphere_offset_surface(face: &FaceRecord, distance: f64) -> Result<Option<NurbsSurface>, KernelRefusal> {
462    // Escape hatch, the same shape as `BREP_NO_SPHERE_CHARTS`: it makes the
463    // closed form and the collocation fit two cells of one binary, so a
464    // measurement can separate this lane's effect from everything else.
465    if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
466        return Ok(None);
467    }
468    let source = &face.surface;
469    let Some((centre, radius)) = source.analytic().and_then(|shape| shape.sphere_geometry()) else {
470        return Ok(None);
471    };
472    let ([u0, u1], [v0, v1]) = domains(source)?;
473    // `offset_surface`'s positive distance moves OPPOSITE the face normal while
474    // the evaluator's moves ALONG it; the negation is the fit's own, reused
475    // here rather than re-derived (audit §4.1's hand negations get no new one).
476    // A normal this lane cannot read is a DECLINE, not an error: the fit below
477    // samples the same evaluator across the whole Greville grid and has its own
478    // singular-row recovery, so anything that offset before must still offset.
479    let Ok(sample) = OffsetEvaluator::new(
480        "offset_surface",
481        source,
482        OffsetNormal::FaceStable {
483            same_sense: face.same_sense,
484        },
485    )
486    .at((u0 + u1) / 2.0, (v0 + v1) / 2.0, -distance)
487    else {
488        return Ok(None);
489    };
490    let source_radial = sample.source.sub(centre);
491    let offset_radial = sample.point.sub(centre);
492    let offset_radius = offset_radial.length();
493    let scale = radius.max(1.0);
494    if radius <= 1e-12
495        || offset_radius <= 1e-9 * scale
496        // Offset through the centre and out the far side: the concentric mate
497        // is a sphere turned inside out, not this construction's answer.
498        || offset_radial.dot(source_radial) <= 0.0
499        // The move must be radial and exactly |d| long. A recognized centre
500        // that is not the real one fails here before it can scale anything.
501        || ((offset_radius - radius).abs() - distance.abs()).abs() > 1e-9 * scale
502    {
503        return Ok(None);
504    }
505    let factor = offset_radius / radius;
506    // A zero-weight control has no Euclidean point to scale. The fit lane never
507    // asks one for a position, so declining keeps this lane strictly additive.
508    let Ok(controls) = source
509        .control_points
510        .iter()
511        .map(|row| {
512            row.iter()
513                .map(|control| {
514                    Ok(Vec4::from_point(
515                        centre.add(control.point().or_refuse(KernelStage::Refine, "point")?.sub(centre).scale(factor)),
516                        control.w,
517                    ))
518                })
519                .collect::<Result<Vec<_>, KernelRefusal>>()
520        })
521        .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
522    else {
523        return Ok(None);
524    };
525    let Ok(surface) = NurbsSurface::new(
526        source.degree_u,
527        source.degree_v,
528        source.knots_u.clone(),
529        source.knots_v.clone(),
530        controls,
531    ) else {
532        return Ok(None);
533    };
534    // The claim, tested. A net that does not land on the pointwise offset
535    // everywhere is not this lane's surface, whatever it was recognized as —
536    // and a measurement that could not be taken is not a pass.
537    match measure_surface_fit_against_pointwise_offset(
538        source,
539        face.same_sense,
540        &surface,
541        distance,
542        1e-9 * scale,
543    ) {
544        Ok(measured) if measured.within_band() => Ok(Some(surface)),
545        _ => Ok(None),
546    }
547}
548
549/// The EXACT offset of a surface of REVOLUTION whose generatrix is ONE CIRCLE
550/// ARC — a torus of any kind (ring, horn, spindle), over a full or a partial
551/// sweep — or `None` for every other carrier.
552///
553/// Why it is exact. A revolution's normal lies in the meridian plane and equals
554/// the generatrix's own in-plane normal there, so the offset surface IS the
555/// revolution of the offset generatrix, and the offset of a circle arc of
556/// radius `r` about `c` is the concentric arc of radius `r ± d`. On the net:
557/// row `i` of a revolution's control net is an AFFINE image `A_i` of the
558/// generatrix's controls — a rotation for the on-circle rows, but a rotation
559/// STRETCHED radially by `1/cos` of the half-span for the mid rows, which is
560/// not a similarity. Affine maps commute with scaling about a point, so scaling
561/// row `i` about `C_i = A_i(c)` by `(r ± d)/r` is exactly `A_i` applied to the
562/// offset circle's net; that commutation is the only reason the stretched rows
563/// come out right. `C_i` is found without reading `A_i` at all: `c` is an affine
564/// combination of three generatrix controls, and the same combination of row
565/// `i`'s controls is `A_i(c)`. Knots, degrees and weights are untouched, so the
566/// same `(u, v)` still names the corresponding point and cloned trims land
567/// where they did, as on [`sphere_offset_surface`].
568///
569/// Everything is computed and then tested, never claimed. The generatrix is
570/// row 0 of the net, and it must lie on one circle to rounding. The sign and
571/// size of the move come from the same [`OffsetEvaluator`] the fit samples,
572/// read at a generatrix point against the circle's centre: the move must be
573/// radial and exactly `|d|` long, and an offset through the centre (`r − d ≤
574/// 0`, where the offset circle turns inside out) declines. The finished net
575/// must land on the pointwise offset everywhere on the measurement grid, to
576/// `1e-9` of the model's scale. Every decline falls through to the fit.
577///
578/// Measured on the partial lemon (a 106° arc of radius 5 centred 3 off the
579/// axis) shelled inward by 1: the fitted carrier sat 1.4e-5 off the pointwise
580/// offset and its shell 3.6e-7 short of the Pappus form in volume.
581fn revolved_arc_offset_surface(
582    face: &FaceRecord,
583    distance: f64,
584) -> Result<Option<NurbsSurface>, KernelRefusal> {
585    if std::env::var_os("BREP_NO_ANALYTIC_OFFSET").is_some() {
586        return Ok(None);
587    }
588    let source = &face.surface;
589    match source.analytic() {
590        Some(crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. }) => {}
591        _ => return Ok(None),
592    }
593    let rows = &source.control_points;
594    let Some(first_row) = rows.first() else {
595        return Ok(None);
596    };
597    if rows.iter().flatten().any(|control| !(control.w > 0.0)) {
598        return Ok(None);
599    }
600    let Ok(generatrix) = NurbsCurve::new(source.degree_v, source.knots_v.clone(), first_row.clone())
601    else {
602        return Ok(None);
603    };
604    let [t0, t1] = generatrix.domain().or_refuse(KernelStage::Refine, "domain")?;
605    let at = |fraction: f64| generatrix.evaluate(t0 + (t1 - t0) * fraction);
606    // The circle through three generatrix points, then every sample held to it.
607    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")?);
608    let (ab, ae) = (b.sub(a), e.sub(a));
609    let normal = ab.cross(ae);
610    let normal_length_squared = normal.dot(normal);
611    if normal_length_squared <= 1e-24 {
612        return Ok(None);
613    }
614    let centre = a.add(
615        ae.cross(normal)
616            .scale(ab.dot(ab))
617            .add(normal.cross(ab).scale(ae.dot(ae)))
618            .scale(0.5 / normal_length_squared),
619    );
620    let radius = a.sub(centre).length();
621    let scale = (centre.length() + radius).max(1.0);
622    let band = 1e-9 * scale;
623    let unit_normal = normal.scale(1.0 / normal_length_squared.sqrt());
624    let samples = 16 * generatrix.control_points.len();
625    for index in 0..=samples {
626        let point = at(index as f64 / samples as f64).or_refuse(KernelStage::Refine, "at")?;
627        let radial = point.sub(centre);
628        if (radial.length() - radius).abs() > band || radial.dot(unit_normal).abs() > band {
629            return Ok(None);
630        }
631    }
632    // `centre` as an affine combination of three generatrix controls, the best
633    // conditioned pair against the first.
634    let controls = first_row
635        .iter()
636        .map(|control| control.point().or_refuse(KernelStage::Refine, "point"))
637        .collect::<Result<Vec<_>, KernelRefusal>>()?;
638    let origin = controls[0];
639    let mut best = (0usize, 0usize, 0.0f64);
640    for first in 1..controls.len() {
641        for second in first + 1..controls.len() {
642            let area = controls[first]
643                .sub(origin)
644                .cross(controls[second].sub(origin))
645                .length();
646            if area > best.2 {
647                best = (first, second, area);
648            }
649        }
650    }
651    let (first, second, area) = best;
652    if area <= 1e-9 * scale * scale {
653        return Ok(None);
654    }
655    let (along_first, along_second) = (controls[first].sub(origin), controls[second].sub(origin));
656    let offset_from_origin = centre.sub(origin);
657    let (g11, g12, g22) = (
658        along_first.dot(along_first),
659        along_first.dot(along_second),
660        along_second.dot(along_second),
661    );
662    let determinant = g11 * g22 - g12 * g12;
663    let (r1, r2) = (along_first.dot(offset_from_origin), along_second.dot(offset_from_origin));
664    let s = (r1 * g22 - r2 * g12) / determinant;
665    let t = (r2 * g11 - r1 * g12) / determinant;
666    if origin
667        .add(along_first.scale(s))
668        .add(along_second.scale(t))
669        .sub(centre)
670        .length()
671        > band
672    {
673        return Ok(None);
674    }
675    // The move, read off the shared evaluator on the generatrix row. The
676    // negation is the fit's own, as in `sphere_offset_surface`.
677    let ([u0, _], [v0, v1]) = domains(source)?;
678    let Ok(sample) = OffsetEvaluator::new(
679        "offset_surface",
680        source,
681        OffsetNormal::FaceStable {
682            same_sense: face.same_sense,
683        },
684    )
685    .at(u0, (v0 + v1) / 2.0, -distance)
686    else {
687        return Ok(None);
688    };
689    let source_radial = sample.source.sub(centre);
690    let offset_radial = sample.point.sub(centre);
691    let offset_radius = offset_radial.length();
692    if (source_radial.length() - radius).abs() > band
693        || offset_radius <= band
694        || offset_radial.dot(source_radial) <= 0.0
695        || ((offset_radius - radius).abs() - distance.abs()).abs() > band
696    {
697        return Ok(None);
698    }
699    let factor = offset_radius / radius;
700    let Ok(offset_rows) = rows
701        .iter()
702        .map(|row| {
703            let points = row.iter().map(|control| control.point().or_refuse(KernelStage::Refine, "point")).collect::<Result<Vec<_>, KernelRefusal>>()?;
704            let row_origin = points[0];
705            let row_centre = row_origin
706                .add(points[first].sub(row_origin).scale(s))
707                .add(points[second].sub(row_origin).scale(t));
708            Ok(points
709                .iter()
710                .zip(row)
711                .map(|(point, control)| {
712                    Vec4::from_point(row_centre.add(point.sub(row_centre).scale(factor)), control.w)
713                })
714                .collect::<Vec<_>>())
715        })
716        .collect::<Result<Vec<Vec<_>>, KernelRefusal>>()
717    else {
718        return Ok(None);
719    };
720    let Ok(surface) = NurbsSurface::new(
721        source.degree_u,
722        source.degree_v,
723        source.knots_u.clone(),
724        source.knots_v.clone(),
725        offset_rows,
726    ) else {
727        return Ok(None);
728    };
729    match measure_surface_fit_against_pointwise_offset(
730        source,
731        face.same_sense,
732        &surface,
733        distance,
734        band,
735    ) {
736        Ok(measured) if measured.within_band() => Ok(Some(surface)),
737        _ => Ok(None),
738    }
739}
740
741pub(crate) fn offset_surface_with_lane(
742    face: &FaceRecord,
743    distance: f64,
744    extension: &CarrierExtension,
745) -> Result<(NurbsSurface, OffsetSurfaceLane), KernelRefusal> {
746    offset_surface_constructed(face, distance, extension).map(|(surface, lane, _)| (surface, lane))
747}
748
749/// [`offset_surface_with_lane`], plus the surface the carrier is the pointwise
750/// offset OF when that is not the face's own: the source extended along its
751/// rulings, for a ruled extension (see the RULED EXTENSION block).
752fn offset_surface_constructed(
753    face: &FaceRecord,
754    distance: f64,
755    extension: &CarrierExtension,
756) -> Result<(NurbsSurface, OffsetSurfaceLane, Option<NurbsSurface>), KernelRefusal> {
757    let source = &face.surface;
758    let extension_active = extension.is_active();
759    if source.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
760        let ([u0, u1], [v0, v1]) = domains(source)?;
761        let normal = stable_face_normal(face, (u0 + u1) / 2.0, (v0 + v1) / 2.0)?;
762        let shift = normal.scale(-distance);
763        let mut points = source
764            .control_points
765            .iter()
766            .map(|row| {
767                row.iter()
768                    .map(|control| Ok(control.point().or_refuse(KernelStage::Refine, "point")?.add(shift)))
769                    .collect::<Result<Vec<_>, KernelRefusal>>()
770            })
771            .collect::<Result<Vec<_>, KernelRefusal>>()?;
772        if extension_active {
773            let p00 = points[0][0];
774            let p01 = points[0][1];
775            let p10 = points[1][0];
776            let direction_u = p10.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
777            let direction_v = p01.sub(p00).normalized().or_refuse(KernelStage::Refine, "normalized")?;
778            // The net slides while the cloned pcurves stay put, so a trim
779            // that spans only part of the domain would grow by less than
780            // asked (a 20-wide face trimmed to [0,16] by a blend moved its
781            // x=16 edge 0.6 for a 1.0 pad). Solve the slide for the TRIM's
782            // own bounding box instead.
783            let (trim_u, trim_v) = trim_domain_fractions(face, [u0, u1], [v0, v1])?;
784            let (back_u, forward_u) = domain_end_moves(extension.u_min, extension.u_max, trim_u);
785            let (back_v, forward_v) = domain_end_moves(extension.v_min, extension.v_max, trim_v);
786            points[0][0] = p00
787                .sub(direction_u.scale(back_u))
788                .sub(direction_v.scale(back_v));
789            points[0][1] = p01
790                .sub(direction_u.scale(back_u))
791                .add(direction_v.scale(forward_v));
792            points[1][0] = p10
793                .add(direction_u.scale(forward_u))
794                .sub(direction_v.scale(back_v));
795            points[1][1] = points[1][1]
796                .add(direction_u.scale(forward_u))
797                .add(direction_v.scale(forward_v));
798        }
799        let controls = points
800            .into_iter()
801            .enumerate()
802            .map(|(row, points)| {
803                points
804                    .into_iter()
805                    .enumerate()
806                    .map(|(column, point)| {
807                        Vec4::from_point(point, source.control_points[row][column].w)
808                    })
809                    .collect()
810            })
811            .collect();
812        // The extension slides the control net along the plane, so the same
813        // `(u, v)` no longer names the pointwise offset of the same source
814        // point; without it the shift is rigid and exact.
815        let lane = if extension_active {
816            OffsetSurfaceLane::Reparameterised
817        } else {
818            OffsetSurfaceLane::Affine
819        };
820        return Ok((
821            NurbsSurface::new(
822                source.degree_u,
823                source.degree_v,
824                source.knots_u.clone(),
825                source.knots_v.clone(),
826                controls,
827            ).or_refuse(KernelStage::Refine, "new")?,
828            lane,
829            None,
830        ));
831    }
832
833    // A recognized SPHERE has a closed-form concentric offset that needs no
834    // collocation at all. Taken before the fit so the exact answer wins
835    // wherever it exists; `sphere_offset_surface` declines on everything else,
836    // and on a sphere whose construction it cannot verify.
837    //
838    // The extension cannot reach a sphere either way: both blocks below sit
839    // under `parameters_v.len() == 2`, and a sphere's meridian is a degree-2
840    // net of five controls. Taking this lane under an active extension keeps
841    // exactly the behaviour the fit had — no growth — and only sharpens it.
842    if let Some(surface) = sphere_offset_surface(face, distance)? {
843        return Ok((surface, OffsetSurfaceLane::Analytic, None));
844    }
845    // The same holds for a revolved ARC: its generatrix has three or more
846    // controls, so no extension below could reach it.
847    if let Some(surface) = revolved_arc_offset_surface(face, distance)? {
848        return Ok((surface, OffsetSurfaceLane::Analytic, None));
849    }
850
851    let knot_u = KnotVector::new(source.knots_u.clone(), source.degree_u).or_refuse(KernelStage::Refine, "new")?;
852    let knot_v = KnotVector::new(source.knots_v.clone(), source.degree_v).or_refuse(KernelStage::Refine, "new")?;
853    let parameters_u = greville_parameters(&knot_u);
854    let parameters_v = greville_parameters(&knot_v);
855    // The Greville sample grid IS a pointwise offset evaluation — this fit is
856    // the shared evaluator's consumer, not its peer. `offset_surface`'s
857    // positive distance moves OPPOSITE the face normal while the evaluator's
858    // moves ALONG it, so the negation happens once, here, with a name on it
859    // (audit §4.1's four hand negations get no fifth).
860    let evaluator = OffsetEvaluator::new(
861        "offset_surface",
862        source,
863        OffsetNormal::FaceStable {
864            same_sense: face.same_sense,
865        },
866    );
867    let mut samples = Vec::new();
868    for &u in &parameters_u {
869        let mut row = Vec::new();
870        for &v in &parameters_v {
871            row.push(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point);
872        }
873        samples.push(row);
874    }
875    // APEX-CONE PINCH RETRIM: offsetting an apex cone INWARD moves each
876    // ruling past the axis — the sampled far row becomes a ring on the far
877    // side (radius d·cos half-angle, mirrored through the axis) and the
878    // offset surface self-pinches inside the v-domain. The genuine cavity
879    // ends AT the pinch (the offset cone's own apex). For a linear-v net
880    // (two sample rows — every made/booleaned cone) the pinch lies on each
881    // ruling at the fraction where the radial vector vanishes: detect the
882    // inversion (far-row radials anti-parallel to near-row radials about the
883    // row centroids) and pull the far row back to the pinch point, so the
884    // fitted surface ends in a proper degenerate apex row instead of a
885    // parasitic inverted tip ending in an unweldable ring.
886    // Both blocks below move the sample grid OFF the pointwise offset on
887    // purpose. Recording that is what lets `offset_surface_measured` decline to
888    // report a designed divergence as a fit error.
889    let mut reparameterised = false;
890    let mut extended_source: Option<NurbsSurface> = None;
891    // Two u samples are enough for both: a 2 × 2 bilinear net (a planar
892    // trapezoid wall of a lofted square frustum, which the affine test above
893    // rejects on its parallelogram defect) has straight rulings like any
894    // other linear-v net, and without the extension its inward carrier
895    // stopped 0.059 short of the opening the reach lane had measured
896    // (2026-09-26, the SHARP rim corner fixture).
897    if parameters_v.len() == 2 && parameters_u.len() >= 2 {
898        let centroid = |column: usize| {
899            let mut sum = Vec3::default();
900            for row in &samples {
901                sum = sum.add(row[column]);
902            }
903            sum.scale(1.0 / samples.len() as f64)
904        };
905        let near_centroid = centroid(0);
906        let far_centroid = centroid(1);
907        let mut inverted = true;
908        let mut pinch_fraction = 0.0f64;
909        let mut near_mean = 0.0f64;
910        let mut far_mean = 0.0f64;
911        for row in &samples {
912            let near_radial = row[0].sub(near_centroid);
913            let far_radial = row[1].sub(far_centroid);
914            let near_len = near_radial.length();
915            let far_len = far_radial.length();
916            if near_len <= 1e-9 || far_len <= 1e-9 {
917                inverted = false;
918                break;
919            }
920            if near_radial.dot(far_radial) >= 0.0 {
921                inverted = false;
922                break;
923            }
924            pinch_fraction += near_len / (near_len + far_len) / samples.len() as f64;
925            near_mean += near_len / samples.len() as f64;
926            far_mean += far_len / samples.len() as f64;
927        }
928        if inverted {
929            let retrim_far = far_mean <= near_mean;
930            // WHOSE fold is it?  The retrim exists so a cavity that ends at the
931            // offset cone's own apex ends there in the TOPOLOGY too, instead of
932            // running past the pinch into a mirrored ring no weld can use.  It
933            // pays for that by moving the sample grid off the pointwise offset,
934            // and the price is the shared-basis identity every consumer rests
935            // on: after it, the SAME `(u, v)` no longer names the offset of the
936            // same source point.
937            //
938            // That price is only worth paying when the face's TRIM actually
939            // reaches past the pinch.  A trim on a cone's wide half offsets
940            // perfectly well; its carrier folds somewhere the face never goes,
941            // and shearing its parameterization to tidy that away sheared the
942            // trim's own pcurves with it — a 45° cone sheet trimmed to its wide
943            // half shipped a slab 0.86 % off its Pappus volume, validating and
944            // wrong (`thicken.rs`,
945            // `a_re_parameterized_offset_carrier_keeps_the_pointwise_offset`).
946            //
947            // So read the trim, the way the extension below already does.  With
948            // a linear v basis the pinch sits at the v FRACTION
949            // `pinch_fraction`, and the folded side is whichever end the retrim
950            // would have pulled.  A face with no loops still reports the whole
951            // domain, which keeps every full-domain carrier on its exact
952            // previous path.
953            let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
954            let trim_reaches_the_pinch = if retrim_far {
955                trim_v[1] > pinch_fraction + 1e-9
956            } else {
957                trim_v[0] < pinch_fraction - 1e-9
958            };
959            if trim_reaches_the_pinch {
960                // Pull the crossed (smaller-ring, past-the-pinch) end back to
961                // the pinch point on each ruling.
962                reparameterised = true;
963                for row in &mut samples {
964                    let near = row[0];
965                    let far = row[1];
966                    let pinch = near.add(far.sub(near).scale(pinch_fraction));
967                    if retrim_far {
968                        row[1] = pinch;
969                    } else {
970                        row[0] = pinch;
971                    }
972                }
973            }
974        }
975        // RULED EXTENSION: `planar_extension` is a no-op for curved carriers
976        // above, but a cone/cylinder lateral joined at a reflex edge needs
977        // its offset skin to GROW past the source rim exactly like a plane
978        // (a cylinder piercing a cone: the two offsets only meet past both
979        // cloned rims). The carrier's parameterization stays (knots/pcurves
980        // untouched) while its world image — and with it the cloned trim's
981        // image — inflates: the carrier is the pointwise offset of the SOURCE
982        // EXTENDED ALONG ITS RULINGS, which a linear-v net does exactly.
983        //
984        // Stretching the sampled OFFSET rulings instead reaches the same
985        // surface only where the source is DEVELOPABLE — its normal constant
986        // along each ruling, as on a cylinder, a cone or an extruded wall —
987        // because only there is the offset ruled too. A twisted loft is ruled
988        // and not developable: its stretched offset rulings left the offset,
989        // and an outward shell of one validated 0.235 short (1.0 %). So the
990        // stretch is MEASURED against the extended source's pointwise offset,
991        // at both ends and the middle of every sampled ruling, and kept only
992        // within the fit contract; otherwise the extended source is sampled
993        // pointwise, and either way the refinement reads the carrier against it.
994        if extension.extends_v() && !inverted {
995            let mut min_ruling = f64::MAX;
996            let mut back_allowance = f64::MAX;
997            let mut forward_allowance = f64::MAX;
998            let mut extendable = true;
999            for row in &samples {
1000                let ruling = row[1].sub(row[0]);
1001                let length = ruling.length();
1002                min_ruling = min_ruling.min(length);
1003                // Radii about the row centroids expose a converging (conic)
1004                // ruling sheaf; the extension must stop short of its apex or
1005                // the sheet folds through it.
1006                let near_radial = row[0].sub(near_centroid).length();
1007                let far_radial = row[1].sub(far_centroid).length();
1008                if (far_radial - near_radial).abs() > 1e-9 {
1009                    let apex_at = near_radial / (near_radial - far_radial);
1010                    if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1011                        // Apex inside the span: degenerate sheet, do not touch.
1012                        extendable = false;
1013                        break;
1014                    }
1015                    if apex_at < 0.0 {
1016                        back_allowance = back_allowance.min(0.9 * -apex_at);
1017                    } else {
1018                        forward_allowance = forward_allowance.min(0.9 * (apex_at - 1.0));
1019                    }
1020                }
1021            }
1022            // The allowances above are read off the OFFSET rows. The source's
1023            // own ruling sheaf converges at a different rate — an outward
1024            // offset moves a cone's apex FARTHER along the ruling — so a
1025            // growth the offset rows allow can still carry the EXTENDED SOURCE
1026            // through its own apex, and its pointwise offset would then be
1027            // taken on a surface folded back on itself. Read the same radial
1028            // ratio on the source rows and clamp by the tighter of the two.
1029            let mut source_back = f64::MAX;
1030            let mut source_forward = f64::MAX;
1031            if extendable {
1032                let source_centroid = |column: usize| {
1033                    let mut sum = Vec3::default();
1034                    for row in &source.control_points {
1035                        if let Ok(point) = row[column].point() {
1036                            sum = sum.add(point);
1037                        }
1038                    }
1039                    sum.scale(1.0 / source.control_points.len() as f64)
1040                };
1041                let (near_source, far_source) = (source_centroid(0), source_centroid(1));
1042                for row in &source.control_points {
1043                    let (near, far) = (row[0].point().or_refuse(KernelStage::Refine, "point")?, row[1].point().or_refuse(KernelStage::Refine, "point")?);
1044                    let near_radial = near.sub(near_source).length();
1045                    let far_radial = far.sub(far_source).length();
1046                    if (far_radial - near_radial).abs() > 1e-9 {
1047                        let apex_at = near_radial / (near_radial - far_radial);
1048                        if (-1e-9..=1.0 + 1e-9).contains(&apex_at) {
1049                            extendable = false;
1050                            break;
1051                        }
1052                        if apex_at < 0.0 {
1053                            source_back = source_back.min(0.9 * -apex_at);
1054                        } else {
1055                            source_forward = source_forward.min(0.9 * (apex_at - 1.0));
1056                        }
1057                    }
1058                }
1059            }
1060            let (back_allowance, forward_allowance) =
1061                (back_allowance.min(source_back), forward_allowance.min(source_forward));
1062            if extendable && min_ruling > 1e-9 {
1063                // Solved for the TRIM's v-extent, not the domain's: a bore
1064                // face keeps its drill's full-height surface and is trimmed
1065                // to the part it pierces, so moving the surface's own ends
1066                // by |d| moved the trim's rims by only a fraction of that
1067                // (a 30-long ruling trimmed to 20 gave 0.667 for 1.0) — and
1068                // an outward shell's bore never reached the grown planes.
1069                let (_, trim_v) = trim_domain_fractions(face, knot_u.domain(), knot_v.domain())?;
1070                let (back_world, forward_world) =
1071                    domain_end_moves(extension.v_min, extension.v_max, trim_v);
1072                let back = (back_world / min_ruling).min(back_allowance);
1073                let forward = (forward_world / min_ruling).min(forward_allowance);
1074                let stretched = samples
1075                    .iter()
1076                    .map(|row| {
1077                        let ruling = row[1].sub(row[0]);
1078                        vec![row[0].sub(ruling.scale(back)), row[1].add(ruling.scale(forward))]
1079                    })
1080                    .collect::<Vec<_>>();
1081                let extended = extend_along_rulings(source, back, forward)?;
1082                let extended_offsets = OffsetEvaluator::new(
1083                    "offset_surface",
1084                    &extended,
1085                    OffsetNormal::FaceStable {
1086                        same_sense: face.same_sense,
1087                    },
1088                );
1089                let [v0, v1] = knot_v.domain();
1090                let mut developability = 0.0f64;
1091                for (row, &u) in stretched.iter().zip(&parameters_u) {
1092                    for fraction in [0.0, 0.5, 1.0] {
1093                        let exact = extended_offsets.at(u, v0 + (v1 - v0) * fraction, -distance).or_refuse(KernelStage::Refine, "at")?.point;
1094                        let along = row[0].add(row[1].sub(row[0]).scale(fraction));
1095                        developability = developability.max(exact.sub(along).length());
1096                    }
1097                }
1098                let contract = offset_fit_contract(source);
1099                let stretch_kept = developability <= contract;
1100                samples = if stretch_kept {
1101                    stretched
1102                } else {
1103                    parameters_u
1104                        .iter()
1105                        .map(|&u| {
1106                            parameters_v
1107                                .iter()
1108                                .map(|&v| Ok(extended_offsets.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1109                                .collect::<Result<Vec<_>, KernelRefusal>>()
1110                        })
1111                        .collect::<Result<Vec<_>, KernelRefusal>>()?
1112                };
1113                if let Ok(path) = std::env::var("BREP_OFFSET_FIT_TRACE") {
1114                    use std::io::Write;
1115                    let line = format!(
1116                        "extension deg=({},{}) net=({},{}) d={distance:.6} back={back:.6} forward={forward:.6} \
1117                         developability={developability:.3e} contract={contract:.3e} lane={} thread={} args={}\n",
1118                        source.degree_u,
1119                        source.degree_v,
1120                        source.control_points.len(),
1121                        source.control_points[0].len(),
1122                        if stretch_kept { "stretch" } else { "pointwise" },
1123                        std::thread::current().name().unwrap_or("-"),
1124                        std::env::args().skip(1).collect::<Vec<_>>().join(" "),
1125                    );
1126                    if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1127                        let _ = file.write_all(line.as_bytes());
1128                    }
1129                }
1130                extended_source = Some(extended);
1131            }
1132        }
1133    }
1134    let reference = extended_source.as_ref().unwrap_or(source);
1135    let weights = reference
1136        .control_points
1137        .iter()
1138        .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1139        .collect::<Vec<_>>();
1140    let fitted = NurbsSurface::new(
1141        source.degree_u,
1142        source.degree_v,
1143        source.knots_u.clone(),
1144        source.knots_v.clone(),
1145        interpolate_tensor(
1146            &knot_u,
1147            &knot_v,
1148            &parameters_u,
1149            &parameters_v,
1150            &samples,
1151            &weights,
1152        )?,
1153    ).or_refuse(KernelStage::Refine, "new")?;
1154    if reparameterised {
1155        return Ok((fitted, OffsetSurfaceLane::Reparameterised, None));
1156    }
1157    let reference_offsets = OffsetEvaluator::new(
1158        "offset_surface",
1159        reference,
1160        OffsetNormal::FaceStable {
1161            same_sense: face.same_sense,
1162        },
1163    );
1164    Ok((
1165        refine_offset_fit(reference, &reference_offsets, distance, fitted)?,
1166        OffsetSurfaceLane::Fit,
1167        extended_source,
1168    ))
1169}
1170
1171/// A linear-v surface continued along its own rulings by `back` and `forward`
1172/// ruling lengths — the same surface, re-parameterized onto the longer span.
1173/// Exact: each ruling is linear in homogeneous coordinates, so extrapolating the
1174/// homogeneous control rows traces the ruling past both ends. Refused where a
1175/// continued weight stops being positive, which is where the ruling leaves the
1176/// rational patch.
1177fn extend_along_rulings(surface: &NurbsSurface, back: f64, forward: f64) -> Result<NurbsSurface, KernelRefusal> {
1178    let rows = surface
1179        .control_points
1180        .iter()
1181        .map(|row| {
1182            let step = row[1].add(row[0].scale(-1.0));
1183            vec![row[0].add(step.scale(-back)), row[1].add(step.scale(forward))]
1184        })
1185        .collect::<Vec<_>>();
1186    if rows.iter().flatten().any(|point| point.w <= 1e-12) {
1187        return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_ruled_extension_weight", format!(
1188            "offset_surface: extending a ruled face by ({back:.6}, {forward:.6}) of its rulings \
1189             carries a weight through zero; the extended face is not this surface"
1190        )));
1191    }
1192    NurbsSurface::new(surface.degree_u, 1, surface.knots_u.clone(), surface.knots_v.clone(), rows)
1193        .or_refuse(KernelStage::Refine, "new")
1194}
1195
1196/// The fit contract a carrier of `source` is refined toward: the kernel's
1197/// intersection-fit tolerance for the source net's extent.
1198fn offset_fit_contract(source: &NurbsSurface) -> f64 {
1199    let scale = crate::model_scale(
1200        source
1201            .control_points
1202            .iter()
1203            .flatten()
1204            .filter_map(|point| point.point().ok()),
1205    );
1206    crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit
1207}
1208
1209/// Halvings of every knot span [`refine_offset_fit`] may take, per direction.
1210const OFFSET_FIT_REFINEMENTS: usize = 6;
1211
1212/// The largest net one refined collocation may solve: separable weights solve
1213/// one direction at a time, non-separable ones solve the whole tensor densely.
1214const OFFSET_FIT_MAX_CONTROLS_SEPARABLE: usize = 16_384;
1215const OFFSET_FIT_MAX_CONTROLS_TENSOR: usize = 900;
1216
1217/// The out-of-sample deviation of a collocation fit from the pointwise offset,
1218/// read at the MIDPOINTS between consecutive Greville parameters, where the fit
1219/// interpolates nothing: along u (u midpoints on the v Greville rows), along v,
1220/// and between both.
1221#[derive(Clone, Copy, Debug)]
1222struct OffsetFitDeviation {
1223    along_u: f64,
1224    along_v: f64,
1225    between: f64,
1226}
1227
1228impl OffsetFitDeviation {
1229    fn worst(&self) -> f64 {
1230        self.along_u.max(self.along_v).max(self.between)
1231    }
1232}
1233
1234fn greville_midpoints(parameters: &[f64]) -> Vec<f64> {
1235    parameters
1236        .windows(2)
1237        .filter(|pair| pair[1] - pair[0] > 1e-12)
1238        .map(|pair| 0.5 * (pair[0] + pair[1]))
1239        .collect()
1240}
1241
1242fn offset_fit_deviation(
1243    fitted: &NurbsSurface,
1244    evaluator: &OffsetEvaluator,
1245    distance: f64,
1246) -> Result<OffsetFitDeviation, KernelRefusal> {
1247    let parameters_u = greville_parameters(&KnotVector::new(fitted.knots_u.clone(), fitted.degree_u).or_refuse(KernelStage::Refine, "new")?);
1248    let parameters_v = greville_parameters(&KnotVector::new(fitted.knots_v.clone(), fitted.degree_v).or_refuse(KernelStage::Refine, "new")?);
1249    let (middle_u, middle_v) = (greville_midpoints(&parameters_u), greville_midpoints(&parameters_v));
1250    let gap = |u: f64, v: f64| -> Result<f64, KernelRefusal> {
1251        Ok(fitted.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?.sub(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point).length())
1252    };
1253    let mut deviation = OffsetFitDeviation {
1254        along_u: 0.0,
1255        along_v: 0.0,
1256        between: 0.0,
1257    };
1258    for &u in &middle_u {
1259        for &v in &parameters_v {
1260            deviation.along_u = deviation.along_u.max(gap(u, v)?);
1261        }
1262        for &v in &middle_v {
1263            deviation.between = deviation.between.max(gap(u, v)?);
1264        }
1265    }
1266    for &u in &parameters_u {
1267        for &v in &middle_v {
1268            deviation.along_v = deviation.along_v.max(gap(u, v)?);
1269        }
1270    }
1271    Ok(deviation)
1272}
1273
1274/// `surface` with each requested LINEAR direction raised to cubic, the same
1275/// geometry: every span re-expressed as a cubic Bezier on the homogeneous net
1276/// (the points at thirds), joined C0 by triple knots. `None` when a requested
1277/// direction is not linear or carries a repeated interior knot.
1278///
1279/// A linear direction is raised rather than halved because the offset of a
1280/// ruled surface is generally not ruled, and a piecewise-linear fit converges
1281/// only as h^2: the twisted loft's v direction still read 3.3e-6 after six
1282/// halvings, where the cubic basis converges as h^4.
1283fn raise_linear_directions(
1284    surface: &NurbsSurface,
1285    in_u: bool,
1286    in_v: bool,
1287) -> Result<Option<NurbsSurface>, KernelRefusal> {
1288    fn raise(knots: &[f64], row: &[Vec4]) -> Option<(Vec<f64>, Vec<Vec4>)> {
1289        // Degree 1: knots are [k0, k0, k1, ..., kn, kn] for n + 1 controls.
1290        let count = row.len();
1291        if knots.len() != count + 2 {
1292            return None;
1293        }
1294        let breaks = &knots[1..=count];
1295        if breaks.windows(2).any(|pair| pair[1] - pair[0] <= 1e-12) {
1296            return None;
1297        }
1298        let mut raised_knots = vec![breaks[0]; 4];
1299        for &interior in &breaks[1..count - 1] {
1300            raised_knots.extend([interior; 3]);
1301        }
1302        raised_knots.extend([breaks[count - 1]; 4]);
1303        let mut points = Vec::with_capacity(3 * (count - 1) + 1);
1304        for pair in row.windows(2) {
1305            let step = pair[1].add(pair[0].scale(-1.0));
1306            points.push(pair[0]);
1307            points.push(pair[0].add(step.scale(1.0 / 3.0)));
1308            points.push(pair[0].add(step.scale(2.0 / 3.0)));
1309        }
1310        points.push(row[count - 1]);
1311        Some((raised_knots, points))
1312    }
1313    let mut knots_u = surface.knots_u.clone();
1314    let mut knots_v = surface.knots_v.clone();
1315    let mut degree_u = surface.degree_u;
1316    let mut degree_v = surface.degree_v;
1317    let mut net = surface.control_points.clone();
1318    if in_v {
1319        if degree_v != 1 {
1320            return Ok(None);
1321        }
1322        let mut rows = Vec::with_capacity(net.len());
1323        for row in &net {
1324            let Some((knots, points)) = raise(&surface.knots_v, row) else {
1325                return Ok(None);
1326            };
1327            knots_v = knots;
1328            rows.push(points);
1329        }
1330        net = rows;
1331        degree_v = 3;
1332    }
1333    if in_u {
1334        if degree_u != 1 {
1335            return Ok(None);
1336        }
1337        let count_v = net[0].len();
1338        let mut columns = Vec::with_capacity(count_v);
1339        for column in 0..count_v {
1340            let row = net.iter().map(|row| row[column]).collect::<Vec<_>>();
1341            let Some((knots, points)) = raise(&surface.knots_u, &row) else {
1342                return Ok(None);
1343            };
1344            knots_u = knots;
1345            columns.push(points);
1346        }
1347        let count_u = columns[0].len();
1348        net = (0..count_u)
1349            .map(|row| columns.iter().map(|column| column[row]).collect())
1350            .collect();
1351        degree_u = 3;
1352    }
1353    Ok(Some(NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")?))
1354}
1355
1356/// `surface`, the same geometry with a knot inserted at the midpoint of every
1357/// non-empty span of each requested direction (exact Boehm insertion on the
1358/// homogeneous net, so the weights refine with it).
1359fn halve_knot_spans(surface: &NurbsSurface, in_u: bool, in_v: bool) -> Result<NurbsSurface, KernelRefusal> {
1360    fn midpoints(knots: &[f64], degree: usize) -> Vec<f64> {
1361        let last = knots.len() - 1 - degree;
1362        knots[degree..=last]
1363            .windows(2)
1364            .filter(|pair| pair[1] - pair[0] > 1e-12)
1365            .map(|pair| 0.5 * (pair[0] + pair[1]))
1366            .collect()
1367    }
1368    let refine = |degree: usize, knots: &[f64], net: Vec<Vec<Vec4>>| -> Result<(Vec<f64>, Vec<Vec<Vec4>>), KernelRefusal> {
1369        let inserted = midpoints(knots, degree);
1370        let mut refined_knots = knots.to_vec();
1371        let mut rows = Vec::with_capacity(net.len());
1372        for row in net {
1373            let mut curve = NurbsCurve::new(degree, knots.to_vec(), row).or_refuse(KernelStage::Refine, "new")?;
1374            for &parameter in &inserted {
1375                curve = curve.insert_knot(parameter, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
1376            }
1377            refined_knots = curve.knots.clone();
1378            rows.push(curve.control_points);
1379        }
1380        Ok((refined_knots, rows))
1381    };
1382    let mut knots_u = surface.knots_u.clone();
1383    let mut knots_v = surface.knots_v.clone();
1384    let mut net = surface.control_points.clone();
1385    if in_v {
1386        let (knots, rows) = refine(surface.degree_v, &knots_v, net)?;
1387        knots_v = knots;
1388        net = rows;
1389    }
1390    if in_u {
1391        // Transpose so each u column is a curve, refine, transpose back.
1392        let count_v = net[0].len();
1393        let columns = (0..count_v)
1394            .map(|column| net.iter().map(|row| row[column]).collect::<Vec<_>>())
1395            .collect::<Vec<_>>();
1396        let (knots, refined) = refine(surface.degree_u, &knots_u, columns)?;
1397        knots_u = knots;
1398        let count_u = refined[0].len();
1399        net = (0..count_u)
1400            .map(|row| refined.iter().map(|column| column[row]).collect())
1401            .collect();
1402    }
1403    NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
1404}
1405
1406/// A collocation fit of the pointwise offset, refined until its OUT-OF-SAMPLE
1407/// deviation meets the kernel's intersection-fit contract.
1408///
1409/// The Greville fit interpolates the pointwise offset in the SOURCE's basis.
1410/// That is exact wherever the offset lives in that basis — planes, cylinders,
1411/// cones, spheres, tori, which is why it was built that way — and, on a
1412/// free-form face, only as good as the source's span count: a single-span cubic
1413/// Bezier wall offset by 0.3 was measured 1.2e-2 to 1.7e-2 off its pointwise
1414/// offset, with the fit exact at its four Greville rows and nowhere else.
1415///
1416/// Refinement halves the knot spans of the direction the deviation is read in
1417/// (exact knot insertion, so the parameterization does not move: the same
1418/// `(u, v)` still names the offset of the same source point, and every trim
1419/// pcurve cloned onto the carrier stays valid) and re-fits. A fit already under
1420/// the bar is returned untouched, bit for bit. The bar is the one a carrier's
1421/// EDGES are already held to (`offset_face_carrier_impl`'s `fit_tolerance`),
1422/// derived from the source net's extent.
1423///
1424/// The contract is a TARGET; the offset construction band is the GATE. A fit
1425/// that is still over the contract when the rounds run out
1426/// ([`OFFSET_FIT_REFINEMENTS`]), when a round stops improving, or when the next
1427/// net would pass the solver ceilings, is ACCEPTED as its best measured round if
1428/// that round is inside `offset_construction_band` — the allowance every
1429/// offset construction in this family is already judged against — and refused
1430/// by name if it is not. Accepted over the contract is a decision, not a
1431/// silence: `offset_surface_measured` reports the deviation, and
1432/// `a_fit_the_rounds_leave_over_the_contract_is_accepted_inside_the_band` pins
1433/// it. (Measured on the corpus: two imported `OFFSET_SURFACE`s end their rounds
1434/// at 1.3e-5 and 3.4e-5 against a contract of 2.15e-6 and a band of 0.107.)
1435///
1436/// Cost is bounded by the rounds: a direction's spans grow at most 64-fold, a
1437/// round costs one pointwise offset per Greville node plus about four per node
1438/// to measure, and one dense collocation solve per row and per column
1439/// (`n_v·n_u³ + n_u·n_v³` for separable weights), and the rounds' costs are
1440/// geometric, so the last round is about half of the whole.
1441fn refine_offset_fit(
1442    source: &NurbsSurface,
1443    evaluator: &OffsetEvaluator,
1444    distance: f64,
1445    fitted: NurbsSurface,
1446) -> Result<NurbsSurface, KernelRefusal> {
1447    let scale = crate::model_scale(
1448        source
1449            .control_points
1450            .iter()
1451            .flatten()
1452            .filter_map(|point| point.point().ok()),
1453    );
1454    let bar = crate::KernelTolerances::for_scale(scale, 1e-7).intersection_fit;
1455    let trace = std::env::var("BREP_OFFSET_FIT_TRACE").ok();
1456    let started = web_time::Instant::now();
1457    let enabled = std::env::var("BREP_OFFSET_FIT_REFINE").as_deref() != Ok("0");
1458    let Ok(initial) = offset_fit_deviation(&fitted, evaluator, distance) else {
1459        return Ok(fitted);
1460    };
1461    let mut best = fitted;
1462    let mut best_deviation = initial;
1463    let mut refined_source = source.clone();
1464    let mut rounds = 0usize;
1465    let mut stop = "under bar";
1466    if enabled {
1467        while best_deviation.worst() > bar {
1468            if rounds == OFFSET_FIT_REFINEMENTS {
1469                stop = "refinement limit";
1470                break;
1471            }
1472            let (along_u, along_v) = (best_deviation.along_u > bar, best_deviation.along_v > bar);
1473            let (in_u, in_v) = if along_u || along_v {
1474                (along_u, along_v)
1475            } else {
1476                (true, true)
1477            };
1478            let (raise_u, raise_v) = (in_u && refined_source.degree_u == 1, in_v && refined_source.degree_v == 1);
1479            let raised = if raise_u || raise_v {
1480                raise_linear_directions(&refined_source, raise_u, raise_v)?
1481            } else {
1482                None
1483            };
1484            let (halve_u, halve_v) = match raised {
1485                Some(_) => (in_u && !raise_u, in_v && !raise_v),
1486                None => (in_u, in_v),
1487            };
1488            let raised = raised.unwrap_or_else(|| refined_source.clone());
1489            let candidate_source = if halve_u || halve_v {
1490                halve_knot_spans(&raised, halve_u, halve_v)?
1491            } else {
1492                raised
1493            };
1494            let weights = candidate_source
1495                .control_points
1496                .iter()
1497                .map(|row| row.iter().map(|point| point.w).collect::<Vec<_>>())
1498                .collect::<Vec<_>>();
1499            let controls = weights.len() * weights[0].len();
1500            let ceiling = if separable_weights(&weights).is_some() {
1501                OFFSET_FIT_MAX_CONTROLS_SEPARABLE
1502            } else {
1503                OFFSET_FIT_MAX_CONTROLS_TENSOR
1504            };
1505            if controls > ceiling {
1506                stop = "net ceiling";
1507                break;
1508            }
1509            let knot_u = KnotVector::new(candidate_source.knots_u.clone(), candidate_source.degree_u).or_refuse(KernelStage::Refine, "new")?;
1510            let knot_v = KnotVector::new(candidate_source.knots_v.clone(), candidate_source.degree_v).or_refuse(KernelStage::Refine, "new")?;
1511            let parameters_u = greville_parameters(&knot_u);
1512            let parameters_v = greville_parameters(&knot_v);
1513            let samples = parameters_u
1514                .iter()
1515                .map(|&u| {
1516                    parameters_v
1517                        .iter()
1518                        .map(|&v| Ok(evaluator.at(u, v, -distance).or_refuse(KernelStage::Refine, "at")?.point))
1519                        .collect::<Result<Vec<_>, KernelRefusal>>()
1520                })
1521                .collect::<Result<Vec<_>, KernelRefusal>>();
1522            let Ok(samples) = samples else {
1523                stop = "unevaluable sample";
1524                break;
1525            };
1526            let candidate = NurbsSurface::new(
1527                candidate_source.degree_u,
1528                candidate_source.degree_v,
1529                candidate_source.knots_u.clone(),
1530                candidate_source.knots_v.clone(),
1531                interpolate_tensor(&knot_u, &knot_v, &parameters_u, &parameters_v, &samples, &weights)?,
1532            ).or_refuse(KernelStage::Refine, "new")?;
1533            let Ok(deviation) = offset_fit_deviation(&candidate, evaluator, distance) else {
1534                stop = "unmeasurable round";
1535                break;
1536            };
1537            rounds += 1;
1538            if deviation.worst() >= best_deviation.worst() {
1539                stop = "stopped improving";
1540                break;
1541            }
1542            best = candidate;
1543            best_deviation = deviation;
1544            refined_source = candidate_source;
1545        }
1546    } else if best_deviation.worst() > bar {
1547        stop = "refinement disabled";
1548    }
1549    // The gate. Refinement aims at the fit contract; a carrier it could not
1550    // bring inside the offset family's own acceptance band is refused by name
1551    // rather than handed on — a fit measured six orders past its band used to be
1552    // recorded and used anyway.
1553    let band = offset_construction_band(scale);
1554    let refused = enabled && best_deviation.worst() > band;
1555    if let Some(path) = trace {
1556        use std::io::Write;
1557        let caller = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
1558        let thread = std::thread::current().name().unwrap_or("-").to_string();
1559        let line = format!(
1560            "fit deg=({},{}) net=({},{})->({},{}) d={distance:.6} scale={scale:.4} bar={bar:.3e} \
1561             initial=(u {:.3e}, v {:.3e}, uv {:.3e}) final={:.3e} rounds={rounds} stop={stop} \
1562             refused={refused} seconds={:.4} thread={thread} args={caller}\n",
1563            source.degree_u,
1564            source.degree_v,
1565            source.control_points.len(),
1566            source.control_points[0].len(),
1567            best.control_points.len(),
1568            best.control_points[0].len(),
1569            initial.along_u,
1570            initial.along_v,
1571            initial.between,
1572            best_deviation.worst(),
1573            started.elapsed().as_secs_f64(),
1574        );
1575        if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1576            let _ = file.write_all(line.as_bytes());
1577        }
1578    }
1579    if refused {
1580        return Err(KernelRefusal::non_convergence(KernelStage::Refine, "offset_carrier_fit", format!(
1581            "offset_surface: the fitted offset carrier stays {:.3e} off its pointwise offset at \
1582             distance {distance} after {rounds} refinement round(s) ({stop}), past the offset \
1583             construction band {band:.3e}",
1584            best_deviation.worst()
1585        )));
1586    }
1587    Ok(best)
1588}
1589
1590/// Two offset sheets of one source, re-expressed on ONE basis without moving
1591/// either: a linear direction facing a cubic one is raised, then each sheet
1592/// takes the knots the other has and it lacks (exact insertion).
1593///
1594/// [`refine_offset_fit`] refines each carrier by its own measured deviation, so
1595/// two sheets of the same source at different distances can leave on different
1596/// nets; a consumer that pairs their iso curves control point for control point
1597/// (`thicken`'s ruled walls) needs them on the same one. Weights follow the
1598/// insertions, and both sheets carry the source's weights refined the same way.
1599pub(crate) fn unify_offset_sheet_bases(
1600    first: &NurbsSurface,
1601    second: &NurbsSurface,
1602) -> Result<(NurbsSurface, NurbsSurface), KernelRefusal> {
1603    fn raise_to(surface: &NurbsSurface, degree_u: usize, degree_v: usize) -> Result<NurbsSurface, KernelRefusal> {
1604        let raise_u = surface.degree_u == 1 && degree_u == 3;
1605        let raise_v = surface.degree_v == 1 && degree_v == 3;
1606        if !raise_u && !raise_v {
1607            return Ok(surface.clone());
1608        }
1609        raise_linear_directions(surface, raise_u, raise_v)?
1610            .ok_or_else(|| {
1611                KernelRefusal::internal(
1612                    KernelStage::Refine,
1613                    "offset_raise_linear",
1614                    "offset sheets: a linear direction could not be raised",
1615                )
1616            })
1617    }
1618    let degree_u = first.degree_u.max(second.degree_u);
1619    let degree_v = first.degree_v.max(second.degree_v);
1620    let mut first = raise_to(first, degree_u, degree_v)?;
1621    let mut second = raise_to(second, degree_u, degree_v)?;
1622    if first.degree_u != second.degree_u || first.degree_v != second.degree_v {
1623        return Err(KernelRefusal::internal(KernelStage::Refine, "offset_common_basis", format!(
1624            "offset sheets: degrees ({},{}) and ({},{}) have no common basis",
1625            first.degree_u, first.degree_v, second.degree_u, second.degree_v
1626        )));
1627    }
1628    // Knots one vector has beyond the other, counted with multiplicity.
1629    fn missing(from: &[f64], into: &[f64]) -> Vec<f64> {
1630        let mut extra = Vec::new();
1631        let mut index = 0usize;
1632        for &knot in from {
1633            while index < into.len() && into[index] < knot - 1e-12 {
1634                index += 1;
1635            }
1636            if index < into.len() && (into[index] - knot).abs() <= 1e-12 {
1637                index += 1;
1638            } else {
1639                extra.push(knot);
1640            }
1641        }
1642        extra
1643    }
1644    fn insert(surface: &NurbsSurface, knots: &[f64], along_u: bool) -> Result<NurbsSurface, KernelRefusal> {
1645        if knots.is_empty() {
1646            return Ok(surface.clone());
1647        }
1648        let (degree, vector) = if along_u {
1649            (surface.degree_u, &surface.knots_u)
1650        } else {
1651            (surface.degree_v, &surface.knots_v)
1652        };
1653        let lines = if along_u {
1654            (0..surface.control_points[0].len())
1655                .map(|column| surface.control_points.iter().map(|row| row[column]).collect::<Vec<_>>())
1656                .collect::<Vec<_>>()
1657        } else {
1658            surface.control_points.clone()
1659        };
1660        let mut refined_knots = vector.clone();
1661        let mut refined = Vec::with_capacity(lines.len());
1662        for line in lines {
1663            let mut curve = NurbsCurve::new(degree, vector.clone(), line).or_refuse(KernelStage::Refine, "new")?;
1664            for &knot in knots {
1665                curve = curve.insert_knot(knot, 1).or_refuse(KernelStage::Refine, "insert_knot")?;
1666            }
1667            refined_knots = curve.knots.clone();
1668            refined.push(curve.control_points);
1669        }
1670        let net = if along_u {
1671            (0..refined[0].len())
1672                .map(|row| refined.iter().map(|column| column[row]).collect())
1673                .collect()
1674        } else {
1675            refined
1676        };
1677        let (knots_u, knots_v) = if along_u {
1678            (refined_knots, surface.knots_v.clone())
1679        } else {
1680            (surface.knots_u.clone(), refined_knots)
1681        };
1682        NurbsSurface::new(surface.degree_u, surface.degree_v, knots_u, knots_v, net).or_refuse(KernelStage::Refine, "new")
1683    }
1684    let (to_first_u, to_second_u) = (
1685        missing(&second.knots_u, &first.knots_u),
1686        missing(&first.knots_u, &second.knots_u),
1687    );
1688    first = insert(&first, &to_first_u, true)?;
1689    second = insert(&second, &to_second_u, true)?;
1690    let (to_first_v, to_second_v) = (
1691        missing(&second.knots_v, &first.knots_v),
1692        missing(&first.knots_v, &second.knots_v),
1693    );
1694    first = insert(&first, &to_first_v, false)?;
1695    second = insert(&second, &to_second_v, false)?;
1696    Ok((first, second))
1697}
1698
1699/// The 3D curve of one carrier edge: the image of the source coedge's pcurve
1700/// on the carrier surface, with the parameter range `(t0, t1)` that matches the
1701/// pcurve's domain fraction for fraction (the contract every coedge of this
1702/// carrier relies on, since the source pcurves are reused verbatim).
1703///
1704/// Built through the [`crate::image_curve`] ladder — exact on an affine sheet
1705/// (every planar offset), the exact iso-curve where the pcurve holds one
1706/// coordinate constant (a blend rail, a cylinder's cap rim, every
1707/// extrude/revolve boundary), and a fitted curve verified to `fit_tolerance`
1708/// off its nodes otherwise. Only where the ladder refuses does the edge fall
1709/// back to the degree-1 interpolant through `points` (the historical
1710/// construction), so nothing that offset before is refused now.
1711///
1712/// Why the polyline is no longer the first choice: its adaptive subdivision
1713/// stops at a 5e-4 chord sag, and a 128-chord rim on an r = 8.5 cylinder sits
1714/// up to 1.6e-4 inside the true arc. That is 16x the boolean imprint's
1715/// coincidence band, so when a later operation's section curve runs along that
1716/// rim (a cutter's side wall coplanar with the offset shell's opening wall,
1717/// 2026-09-06 report) the imprint cannot recognise the rim as the section it
1718/// already is, and reports every chord vertex as a crossing — one edge split
1719/// into forty. An exact arc is recognised as coincident and left alone.
1720fn carrier_edge_curve(
1721    surface: &NurbsSurface,
1722    pcurve: &NurbsCurve,
1723    points: &[Vec3],
1724    parameters: &[f64],
1725    fit_tolerance: f64,
1726) -> Result<(NurbsCurve, f64, f64), KernelRefusal> {
1727    // `BREP_CARRIER_POLYLINE=1` restores the polyline for an A/B comparison.
1728    let image = if std::env::var("BREP_CARRIER_POLYLINE").as_deref() == Ok("1") {
1729        Err("BREP_CARRIER_POLYLINE set".to_string())
1730    } else {
1731        crate::image_curve::image_curve(surface, pcurve, fit_tolerance, "offset_face_carrier")
1732    };
1733    match image {
1734        Ok(image) if image.t0 <= image.t1 => Ok((image.curve, image.t0, image.t1)),
1735        Ok(image) => {
1736            // The image runs against the pcurve: reverse it so the edge range
1737            // is increasing, reflecting the range through the curve's domain.
1738            let [start, end] = image.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1739            let curve = image.curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
1740            Ok((curve, start + end - image.t0, start + end - image.t1))
1741        }
1742        Err(_) => {
1743            let curve = interpolate_curve(points, 1, parameters).or_refuse(KernelStage::Refine, "interpolate_curve")?;
1744            let [t0, t1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1745            Ok((curve, t0, t1))
1746        }
1747    }
1748}
1749
1750fn mapped_pcurve_polyline(
1751    surface: &NurbsSurface,
1752    pcurve: &NurbsCurve,
1753    degenerate: bool,
1754) -> Result<(Vec<Vec3>, Vec<f64>), KernelRefusal> {
1755    let [start, end] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1756    let evaluate = |fraction: f64| -> Result<Vec3, KernelRefusal> {
1757        let uv = pcurve.evaluate(start + (end - start) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
1758        surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")
1759    };
1760    let first = evaluate(0.0)?;
1761    let last = evaluate(1.0)?;
1762    if degenerate {
1763        return Ok((vec![first, last], vec![0.0, 1.0]));
1764    }
1765    fn append(
1766        evaluate: &impl Fn(f64) -> Result<Vec3, KernelRefusal>,
1767        a_fraction: f64,
1768        a: Vec3,
1769        b_fraction: f64,
1770        b: Vec3,
1771        depth: usize,
1772        parameters: &mut Vec<f64>,
1773        points: &mut Vec<Vec3>,
1774    ) -> Result<(), KernelRefusal> {
1775        let fractions =
1776            [0.25, 0.5, 0.75].map(|local| a_fraction + (b_fraction - a_fraction) * local);
1777        let samples = fractions
1778            .map(evaluate)
1779            .into_iter()
1780            .collect::<Result<Vec<_>, KernelRefusal>>()?;
1781        let deviation = samples
1782            .iter()
1783            .enumerate()
1784            .map(|(index, point)| {
1785                point
1786                    .sub(a.add(b.sub(a).scale((index + 1) as f64 * 0.25)))
1787                    .length()
1788            })
1789            .fold(0.0, f64::max);
1790        if deviation <= 5e-4 || depth >= 10 {
1791            parameters.push(b_fraction);
1792            points.push(b);
1793            return Ok(());
1794        }
1795        append(
1796            evaluate,
1797            a_fraction,
1798            a,
1799            fractions[1],
1800            samples[1],
1801            depth + 1,
1802            parameters,
1803            points,
1804        )?;
1805        append(
1806            evaluate,
1807            fractions[1],
1808            samples[1],
1809            b_fraction,
1810            b,
1811            depth + 1,
1812            parameters,
1813            points,
1814        )
1815    }
1816    let mut parameters = vec![0.0];
1817    let mut points = vec![first];
1818    append(
1819        &evaluate,
1820        0.0,
1821        first,
1822        1.0,
1823        last,
1824        0,
1825        &mut parameters,
1826        &mut points,
1827    )?;
1828    Ok((points, parameters))
1829}
1830
1831/// Everything an offset carrier's construction MEASURED about itself.
1832///
1833/// The half this kernel lacked: a tolerance MEASURED from the geometry that was
1834/// actually built, rather than a band chosen from `scale` before building. It
1835/// lives in the one place that needs no durable-format change: alongside the
1836/// transient construction result, never as a field on a [`crate::BrepSolid`]
1837/// record. `io/snapshot.rs` is a documented durable format and
1838/// `SOLID_CODEC_VERSION` a versioned wire layout; persisting per-entity
1839/// tolerances is real, planned, and separately designed. This lands the
1840/// measurement with no format churn at all.
1841///
1842/// Every number here is a RECORD. None of it widens a band — see
1843/// [`MeasuredTolerance`]'s direction rule.
1844#[derive(Clone, Debug)]
1845pub struct CarrierDeviation {
1846    /// The derived band every measurement below was judged against:
1847    /// [`crate::offset_construction_band`] of the source solid's extent.
1848    pub band: f64,
1849    /// Which branch built the carrier surface.
1850    pub lane: OffsetSurfaceLane,
1851    /// The carrier surface's own fit error, when the lane has one.
1852    pub surface: Option<MeasuredTolerance>,
1853    /// `max_t ‖C_3d(t) − S_off(p(t))‖` per carrier edge id, folded over every
1854    /// coedge that references the edge — OCCT's `FillEdgeData` rule
1855    /// (`BRepOffset_SimpleOffset.cxx:296-310`), which takes the maximum over
1856    /// **every** adjacent face rather than the first one.
1857    pub edges: Vec<(u64, MeasuredTolerance)>,
1858    /// Per carrier vertex id, propagated from the incident edge ends by
1859    /// [`crate::vertex_tolerance_from_edges`] — which is also where the verdict
1860    /// on OCCT's 1.001 inflation factor is recorded.
1861    pub vertices: Vec<(u64, f64)>,
1862}
1863
1864impl CarrierDeviation {
1865    /// The worst thing the construction did, against the tightest band it
1866    /// faced. `None` only when there was nothing at all to measure.
1867    pub fn worst(&self) -> Option<MeasuredTolerance> {
1868        MeasuredTolerance::worst(
1869            self.surface
1870                .into_iter()
1871                .chain(self.edges.iter().map(|(_, measured)| *measured))
1872                .chain(
1873                    self.vertices
1874                        .iter()
1875                        .map(|(_, gap)| MeasuredTolerance::new(*gap, self.band)),
1876                ),
1877        )
1878    }
1879
1880    /// The entities whose measured deviation exceeded the derived band — the
1881    /// interesting case, and the only one any gate acts on.
1882    pub fn exceedances(&self) -> Vec<String> {
1883        let mut out = Vec::new();
1884        if let Some(surface) = self.surface {
1885            if surface.exceeds_band() {
1886                out.push(format!("carrier surface fit {}", surface.describe()));
1887            }
1888        }
1889        for (id, measured) in &self.edges {
1890            if measured.exceeds_band() {
1891                out.push(format!("edge {id} {}", measured.describe()));
1892            }
1893        }
1894        for (id, gap) in &self.vertices {
1895            let measured = MeasuredTolerance::new(*gap, self.band);
1896            if measured.exceeds_band() {
1897                out.push(format!("vertex {id} {}", measured.describe()));
1898            }
1899        }
1900        out
1901    }
1902}
1903
1904#[derive(Clone, Debug, Serialize)]
1905pub struct OffsetFaceCarrier {
1906    pub vertices: Vec<VertexRecord>,
1907    pub edges: Vec<EdgeRecord>,
1908    pub face: FaceRecord,
1909    /// What the construction measured about itself, or `None` when it was built
1910    /// through the unmeasured [`offset_face_carrier`] entry point.
1911    ///
1912    /// `#[serde(skip)]` on purpose: this struct crosses the wasm ABI as JSON
1913    /// (`abi/modeling_b.rs:500`), and a measurement is a diagnostic about a
1914    /// build, not part of the carrier the caller asked for. Skipping it keeps
1915    /// that payload byte-identical.
1916    #[serde(skip)]
1917    pub deviation: Option<CarrierDeviation>,
1918}
1919
1920fn claim_vertex_image(
1921    source_id: u64,
1922    point: Vec3,
1923    vertex_images: &mut HashMap<u64, u64>,
1924    vertices: &mut Vec<VertexRecord>,
1925    next_id: &mut u64,
1926) -> u64 {
1927    if let Some(id) = vertex_images.get(&source_id) {
1928        return *id;
1929    }
1930    let id = *next_id;
1931    *next_id += 1;
1932    vertices.push(VertexRecord { id, point });
1933    vertex_images.insert(source_id, id);
1934    id
1935}
1936
1937pub fn offset_face_carrier(
1938    solid: &BrepSolid,
1939    face_id: u64,
1940    distance: f64,
1941    planar_extension: f64,
1942) -> Result<OffsetFaceCarrier, KernelRefusal> {
1943    offset_face_carrier_impl(
1944        solid,
1945        face_id,
1946        distance,
1947        &CarrierExtension::uniform(planar_extension),
1948        false,
1949    )
1950}
1951
1952/// [`offset_face_carrier`] with a per-side [`CarrierExtension`].
1953pub fn offset_face_carrier_sided(
1954    solid: &BrepSolid,
1955    face_id: u64,
1956    distance: f64,
1957    extension: &CarrierExtension,
1958) -> Result<OffsetFaceCarrier, KernelRefusal> {
1959    offset_face_carrier_impl(solid, face_id, distance, extension, false)
1960}
1961
1962/// [`offset_face_carrier`], with every entity it builds measured against the
1963/// deviation it was meant to reproduce.
1964///
1965/// The carrier is bit-identical to [`offset_face_carrier`]'s — same body, same
1966/// arithmetic, in the same order — with a read-only measurement pass appended.
1967/// Three quantities land in [`CarrierDeviation`]:
1968///
1969/// * the carrier SURFACE's fit against the pointwise offset it interpolates
1970///   ([`offset_surface_measured`]);
1971/// * each carrier EDGE's 3D curve against the locus its pcurve traces on that
1972///   surface — the trim boundary is built by sampling exactly that composition
1973///   and interpolating the images, so this is the construction's own claim,
1974///   measured rather than assumed;
1975/// * each carrier VERTEX, propagated from the incident edge ends.
1976///
1977/// The interesting output is [`CarrierDeviation::exceedances`]: an entity whose
1978/// measured deviation is worse than the size-derived band assumed. That is a
1979/// construction that went wrong in a way the derived band alone cannot see, and
1980/// it is the case a caller should refuse on rather than ship.
1981pub fn offset_face_carrier_measured(
1982    solid: &BrepSolid,
1983    face_id: u64,
1984    distance: f64,
1985    planar_extension: f64,
1986) -> Result<OffsetFaceCarrier, KernelRefusal> {
1987    offset_face_carrier_impl(
1988        solid,
1989        face_id,
1990        distance,
1991        &CarrierExtension::uniform(planar_extension),
1992        true,
1993    )
1994}
1995
1996/// The measured deviations of one built carrier: surface fit, per edge, per
1997/// vertex.
1998///
1999/// Read-only over what the construction produced. The edge measurement is
2000/// [`crate::measure_edge_against_pcurve_image`] — validate's own
2001/// `adaptive_coedge_error` floored by a span-midpoint pass, because the sampler
2002/// alone is aliased against exactly the curves this construction builds (see
2003/// that module's doc) — taken against the far tighter
2004/// [`crate::offset_construction_band`] instead of the vendor-forgiving
2005/// `pcurve_acceptance` validate will use later.
2006fn measure_carrier(
2007    surface: &NurbsSurface,
2008    vertices: &[VertexRecord],
2009    edges: &[EdgeRecord],
2010    loops: &[LoopRecord],
2011    lane: OffsetSurfaceLane,
2012    surface_fit: Option<MeasuredTolerance>,
2013    band: f64,
2014) -> Result<CarrierDeviation, KernelRefusal> {
2015    let edge_by_id: HashMap<u64, &EdgeRecord> = edges.iter().map(|edge| (edge.id, edge)).collect();
2016    // Fold over coedges, not edges: a seam edge is referenced twice with two
2017    // different pcurves, and OCCT's `FillEdgeData` takes the maximum over every
2018    // adjacent face for exactly that reason.
2019    let mut per_edge: HashMap<u64, MeasuredTolerance> = HashMap::default();
2020    for loop_record in loops {
2021        for coedge in &loop_record.coedges {
2022            let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
2023                continue;
2024            };
2025            let measured = measure_edge_against_pcurve_image(
2026                surface,
2027                &coedge.pcurve,
2028                edge,
2029                coedge.forward,
2030                band,
2031            )?;
2032            per_edge
2033                .entry(edge.id)
2034                .and_modify(|existing| *existing = existing.worse_of(measured))
2035                .or_insert(measured);
2036        }
2037    }
2038
2039    let mut measured_edges: Vec<(u64, MeasuredTolerance)> = per_edge.into_iter().collect();
2040    measured_edges.sort_by_key(|(id, _)| *id);
2041    let deviation_of: HashMap<u64, f64> = measured_edges
2042        .iter()
2043        .map(|(id, measured)| (*id, measured.deviation()))
2044        .collect();
2045
2046    // Edge ENDS by the vertex they claim, built once. A degenerate edge claims
2047    // the same vertex at both ends and contributes both, which is right: the
2048    // question is how far every representation meeting there actually lands.
2049    let mut ends_at: HashMap<u64, Vec<(&EdgeRecord, f64)>> = HashMap::default();
2050    for edge in edges {
2051        ends_at
2052            .entry(edge.start_vertex_id)
2053            .or_default()
2054            .push((edge, edge.t0));
2055        ends_at
2056            .entry(edge.end_vertex_id)
2057            .or_default()
2058            .push((edge, edge.t1));
2059    }
2060
2061    let mut measured_vertices = Vec::with_capacity(vertices.len());
2062    for vertex in vertices {
2063        let mut gaps = Vec::new();
2064        let mut incident = Vec::new();
2065        for (edge, parameter) in ends_at.get(&vertex.id).into_iter().flatten() {
2066            gaps.push(vertex_endpoint_gap(
2067                vertex.point,
2068                edge.curve.evaluate(*parameter).or_refuse(KernelStage::Refine, "evaluate")?,
2069            ));
2070            incident.push(deviation_of.get(&edge.id).copied().unwrap_or(0.0));
2071        }
2072        measured_vertices.push((vertex.id, vertex_tolerance_from_edges(gaps, incident)));
2073    }
2074
2075    Ok(CarrierDeviation {
2076        band,
2077        lane,
2078        surface: surface_fit,
2079        edges: measured_edges,
2080        vertices: measured_vertices,
2081    })
2082}
2083
2084fn offset_face_carrier_impl(
2085    solid: &BrepSolid,
2086    face_id: u64,
2087    distance: f64,
2088    extension: &CarrierExtension,
2089    measure: bool,
2090) -> Result<OffsetFaceCarrier, KernelRefusal> {
2091    let source = solid
2092        .shells
2093        .iter()
2094        .flat_map(|shell| &shell.faces)
2095        .find(|face| face.id == face_id)
2096        .ok_or_else(|| format!("offset_face_carrier: missing face {face_id}")).or_refuse(KernelStage::Refine, "offset_missing_face")?;
2097    // The band is derived from the SOURCE SOLID's extent, matching every other
2098    // direct-edit/offset site (`face_offset.rs:103` and its siblings all take
2099    // `solid_model_scale`). The alternative basis is the FACE's own extent — the
2100    // reference shell keys its size-relative offset quantities on the 3D arc
2101    // length of the face's own boundary iso, never on the assembly. Which basis
2102    // is better is unsettled: it is a measurement to make over the refusal
2103    // corpus before switching, not a change to smuggle in here.
2104    let band = offset_construction_band(solid_model_scale(solid));
2105    // The accuracy bar a carrier EDGE's 3D curve must meet against the locus its
2106    // pcurve traces on the carrier surface — the kernel's own SSI fit contract,
2107    // the same bar `thicken` holds its wall boundaries to. NOT the construction
2108    // band: that is the surface-fit allowance (5e-4 of the part), 50x looser
2109    // than the boolean's coincidence band, and an edge fitted only that well is
2110    // what a later imprint mistakes for a crossing (see `carrier_edge_curve`).
2111    let fit_tolerance =
2112        crate::KernelTolerances::for_scale(solid_model_scale(solid), 1e-7).intersection_fit;
2113    let (surface, lane, surface_fit) = if measure {
2114        let measured = offset_surface_measured_sided(source, distance, extension, band)?;
2115        (measured.surface, measured.lane, measured.fit)
2116    } else {
2117        let (surface, lane) = offset_surface_with_lane(source, distance, extension)?;
2118        (surface, lane, None)
2119    };
2120    let source_edges = solid
2121        .edges
2122        .iter()
2123        .map(|edge| (edge.id, edge))
2124        .collect::<HashMap<_, _>>();
2125    let source_vertices = solid
2126        .vertices
2127        .iter()
2128        .map(|vertex| (vertex.id, vertex))
2129        .collect::<HashMap<_, _>>();
2130    let mut vertices = Vec::new();
2131    let mut vertex_images = HashMap::default();
2132    let mut edges = Vec::new();
2133    let mut edge_images = HashMap::default();
2134    let mut loops = Vec::new();
2135    let mut next_id = 1u64;
2136
2137    for source_loop in &source.loops {
2138        let mut coedges = Vec::new();
2139        for source_coedge in &source_loop.coedges {
2140            let source_edge = source_edges
2141                .get(&source_coedge.edge_id)
2142                .ok_or_else(|| "offset_face_carrier: missing source edge".to_string()).or_refuse(KernelStage::Refine, "offset_missing_edge")?;
2143            let (source_start, source_end) = if source_coedge.forward {
2144                (source_edge.start_vertex_id, source_edge.end_vertex_id)
2145            } else {
2146                (source_edge.end_vertex_id, source_edge.start_vertex_id)
2147            };
2148            if !source_vertices.contains_key(&source_start)
2149                || !source_vertices.contains_key(&source_end)
2150            {
2151                return Err(KernelRefusal::internal(KernelStage::Refine, "offset_missing_vertex", "offset_face_carrier: missing source vertex"));
2152            }
2153            // Map even DEGENERATE source edges through the full polyline: a
2154            // cone apex's image on the offset surface is a genuine CIRCLE
2155            // (radius d·cos half-angle), not a point — shortcutting to the
2156            // two endpoints would collapse the ring and leave the carrier's
2157            // topology inconsistent with its surface. Edges whose image truly
2158            // collapses (sphere poles, planar corners) still interpolate to a
2159            // point-sized curve and keep their degenerate flag below.
2160            // Map even DEGENERATE source edges through the full polyline: an
2161            // EXTERIOR cone offset turns the apex point into a genuine RING
2162            // (radius d·cos half-angle) — shortcutting to the endpoints would
2163            // collapse it and leave the carrier topology inconsistent with
2164            // its surface (and the ring imprint would be dropped as
2165            // boundary-coincident with a "degenerate" edge). Images that
2166            // truly collapse (sphere poles; interior apexes after the pinch
2167            // retrim) stay degenerate below. The threshold scales with the
2168            // offset distance: a real ring measures ~d·cos α, while fitted
2169            // pole rows wobble ~1e-4 absolute.
2170            let (points, parameters) =
2171                mapped_pcurve_polyline(&surface, &source_coedge.pcurve, false)?;
2172            let collapse_tolerance = 1e-6f64.max(distance.abs() * 1e-2);
2173            let image_collapsed = points
2174                .iter()
2175                .all(|point| point.sub(points[0]).length() <= collapse_tolerance);
2176            let (edge_id, forward) =
2177                if let Some((edge_id, edge_start_vertex_id, creator_forward)) =
2178                    edge_images.get(&source_edge.id)
2179                {
2180                    if source_start == source_end {
2181                        // A CLOSED source edge (a seam of a periodic face: both
2182                        // ends are the same vertex) is referenced twice by the
2183                        // same loop, once per seam side, and the two references
2184                        // traverse it in OPPOSITE senses — that is what closes
2185                        // the loop. The endpoint test below cannot see that:
2186                        // both ends map to the same vertex image, so it answers
2187                        // `true` for both references and the returning coedge
2188                        // comes back mis-oriented.
2189                        //
2190                        // Measured on a full torus (one vertex, two seam edges):
2191                        // the source records `forward: false` on the two
2192                        // returning coedges and validates clean, while the
2193                        // carrier recorded `forward: true` on both and its rim
2194                        // edge measured 12.5 — a whole part diameter — against
2195                        // its own composed pcurve image. The measured tolerance
2196                        // this slice adds is what surfaced it; no existing gate
2197                        // reaches this shape.
2198                        //
2199                        // The source coedge's own sense is the answer: the image
2200                        // edge runs along the CREATING coedge's pcurve, so a
2201                        // later reference runs with it exactly when the two
2202                        // source coedges traverse the source edge the same way.
2203                        (*edge_id, source_coedge.forward == *creator_forward)
2204                    } else {
2205                        // UNCHANGED for every open edge: the image edge's start
2206                        // vertex identifies which way this coedge runs.
2207                        (
2208                            *edge_id,
2209                            vertex_images.get(&source_start) == Some(edge_start_vertex_id),
2210                        )
2211                    }
2212                } else {
2213                    let (curve, t0, t1) = if image_collapsed {
2214                        let curve = NurbsCurve::new(
2215                            1,
2216                            vec![0.0, 0.0, 1.0, 1.0],
2217                            vec![
2218                                Vec4::from_point(points[0], 1.0),
2219                                Vec4::from_point(points[0], 1.0),
2220                            ],
2221                        ).or_refuse(KernelStage::Refine, "new")?;
2222                        (curve, 0.0, 1.0)
2223                    } else {
2224                        carrier_edge_curve(
2225                            &surface,
2226                            &source_coedge.pcurve,
2227                            &points,
2228                            &parameters,
2229                            fit_tolerance,
2230                        )?
2231                    };
2232                    let start_vertex_id = claim_vertex_image(
2233                        source_start,
2234                        points[0],
2235                        &mut vertex_images,
2236                        &mut vertices,
2237                        &mut next_id,
2238                    );
2239                    let end_vertex_id = claim_vertex_image(
2240                        source_end,
2241                        points[points.len() - 1],
2242                        &mut vertex_images,
2243                        &mut vertices,
2244                        &mut next_id,
2245                    );
2246                    let id = next_id;
2247                    next_id += 1;
2248                    edges.push(EdgeRecord {
2249                        id,
2250                        curve,
2251                        t0,
2252                        t1,
2253                        start_vertex_id,
2254                        end_vertex_id,
2255                        // Degenerate exactly when the IMAGE collapsed. A cone
2256                        // apex maps to a real RING on an exterior offset and
2257                        // must carry a real closed edge; a real source edge
2258                        // whose image collapses to a point is a degenerate edge
2259                        // ON THE CARRIER whatever it was on the source.
2260                        //
2261                        // That second half is what the CARVE needs. The new
2262                        // boundary a carve introduces is a piece of the fold
2263                        // locus, which on the source is an ordinary curve — a
2264                        // parallel circle on a cone — and whose whole defining
2265                        // property is that the offset collapses there. Reading
2266                        // the source's flag as well left it a zero-length
2267                        // NON-degenerate closed edge, which no weld can use.
2268                        degenerate: image_collapsed,
2269                        // Image of a named source edge on the offset carrier;
2270                        // suffixed so it cannot collide with the source edge
2271                        // when both faces survive into one solid.
2272                        name: source_edge
2273                            .name
2274                            .as_ref()
2275                            .map(|name| format!("{name}_Offset")),
2276                    });
2277                    edge_images.insert(
2278                        source_edge.id,
2279                        (id, start_vertex_id, source_coedge.forward),
2280                    );
2281                    (id, true)
2282                };
2283            let id = next_id;
2284            next_id += 1;
2285            coedges.push(CoedgeRecord {
2286                id,
2287                edge_id,
2288                forward,
2289                pcurve: source_coedge.pcurve.clone(),
2290            });
2291        }
2292        let id = next_id;
2293        next_id += 1;
2294        loops.push(LoopRecord { id, coedges });
2295    }
2296    let deviation = if measure {
2297        Some(measure_carrier(
2298            &surface,
2299            &vertices,
2300            &edges,
2301            &loops,
2302            lane,
2303            surface_fit,
2304            band,
2305        )?)
2306    } else {
2307        None
2308    };
2309    Ok(OffsetFaceCarrier {
2310        vertices,
2311        edges,
2312        face: FaceRecord {
2313            id: next_id,
2314            surface,
2315            same_sense: source.same_sense,
2316            loops,
2317            name: source.name.as_ref().map(|name| format!("{name}_Offset")),
2318        },
2319        deviation,
2320    })
2321}
2322