Skip to main content

brep_kernel/edit/direct_edit/
face_offset.rs

1use super::*;
2use crate::{KernelRefusal, KernelStage, OrRefuse};
3
4#[path = "face_offset_plan.rs"]
5mod plan;
6use plan::RebuildPlan;
7#[path = "face_offset_tangent.rs"]
8mod tangent;
9
10// ---------------------------------------------------------------------------
11// Push a CURVED analytic face (cylinder / cone) by OFFSETTING its carrier.
12//
13// The methodology (user directive 2026-08-28, the architecture principle made
14// literal): a face is a TRIMMED region of an infinite carrier surface. To push
15// a curved face we OFFSET its untrimmed carrier surface, then re-derive the trim
16// as the INTERSECTIONS of that offset surface with the (unchanged) untrimmed
17// carriers of the neighbour faces — `intersect_analytic_pair` does the surface ∩
18// surface, `build_pcurve_on_surface` re-trims. This is the same engine the
19// boolean imprint uses; here one surface (the pushed face's) is replaced by its
20// offset and the incident edges are recut.
21//
22// Slice 1 scope (honest refusals, never a bad solid): the PUSHED face is a
23// cylinder or cone (`RuledRevolution`); its neighbours across each boundary edge
24// are PLANAR. Ruled/sphere/torus neighbours and free-form pushed faces are
25// deferred.
26// ---------------------------------------------------------------------------
27
28/// The exact analytic offset of a ruled-revolution CARRIER surface (cylinder or
29/// cone) by `signed_distance` along its OUTWARD normal — the untrimmed surface
30/// S′ whose re-intersection with the neighbour carriers gives the new trim.
31///
32/// A normal offset of a ruled revolution is another ruled revolution with the
33/// SAME axis and half-angle: in the (axial z, radial ρ) meridian, the generatrix
34/// line `ρ = rho0 + m·z` (m = dρ/dz = (rho1−rho0)/height; m = 0 for a cylinder)
35/// offsets to the PARALLEL line `ρ = rho0 + m·z + δ·√(1+m²)` — a uniform radial
36/// growth `grow = δ·√(1+m²)` at every z. So S′ is built by revolving the grown
37/// generatrix about the SAME frame, over the SAME axial span `[0, height]` and
38/// seam azimuth (`frame.x_axis`) as the source — the span keeps the neighbour
39/// caps IN the surface's domain (an axis-translation offset would slide the
40/// finite patch off them) and the shared seam meridian stays put.
41///
42/// `signed_distance > 0` grows the surface outward (radius increases). Refuses a
43/// carrier whose grown radius reaches/crosses the axis at either end.
44fn offset_ruled_carrier(
45    surface: &NurbsSurface,
46    signed_distance: f64,
47    tolerance: f64,
48) -> Result<NurbsSurface, KernelRefusal> {
49    let Some(AnalyticSurface::RuledRevolution {
50        frame,
51        rho0,
52        rho1,
53        height,
54    }) = surface.analytic()
55    else {
56        return Err(KernelRefusal::internal(
57            KernelStage::Classify,
58            "carrier_kind",
59            "offset_ruled_carrier: face carrier is not a ruled revolution",
60        ));
61    };
62    let (frame, rho0, rho1, height) = (frame.clone(), *rho0, *rho1, *height);
63    let slope = (rho1 - rho0) / height;
64    let grow = signed_distance * (1.0 + slope * slope).sqrt();
65    let rho0_new = rho0 + grow;
66    let rho1_new = rho1 + grow;
67    if rho0_new <= tolerance || rho1_new <= tolerance {
68        return Err(KernelRefusal::input(
69            KernelStage::Classify,
70            "axis_collapse",
71            "offset (push): the ruled carrier collapses to or past its axis — refusing",
72        ));
73    }
74    // Revolve the grown generatrix over the SAME axial span + seam azimuth.
75    let start = frame.origin.add(frame.x_axis.scale(rho0_new));
76    let end = frame
77        .origin
78        .add(frame.axis.scale(height))
79        .add(frame.x_axis.scale(rho1_new));
80    let generatrix = make_line(start, end).or_refuse(KernelStage::Classify, "make_line")?;
81    make_revolution(frame.origin, frame.axis, &generatrix, std::f64::consts::TAU)
82        .or_refuse(KernelStage::Classify, "make_revolution")
83}
84
85/// The face's OUTWARD unit normal at its parameter-domain midpoint, oriented by
86/// `same_sense` (the convention `push_face::planar_face_normal` uses).
87fn outward_normal_mid(face: &FaceRecord) -> Result<(Vec3, Vec3), KernelRefusal> {
88    let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Classify, "domain_u")?;
89    let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Classify, "domain_v")?;
90    let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
91    let point = face.surface.evaluate(um, vm).or_refuse(KernelStage::Classify, "evaluate")?;
92    let mut normal = face.surface.normal(um, vm).or_refuse(KernelStage::Classify, "normal")?;
93    if !face.same_sense {
94        normal = normal.scale(-1.0);
95    }
96    Ok((point, normal))
97}
98
99/// Push a CYLINDER or CONE face along its outward normal by `distance` (positive
100/// grows the solid) by OFFSETTING its carrier surface and re-deriving the trim as
101/// the intersection of that offset surface with the neighbour carriers.
102///
103/// Slice-1 scope (honest refusal, never a bad solid): a full-revolution
104/// cylinder/cone SIDE face whose neighbour across every non-seam boundary edge is
105/// PLANAR (its end caps). The pushed face's own periodic SEAM edge rides the
106/// offset carrier's meridian; each rim edge re-intersects its cap
107/// (`intersect_analytic_pair`, exact circle); the caps re-trim as planar faces.
108/// Ruled/curved neighbours and non-full-revolution ruled faces are deferred.
109pub fn offset_ruled_face(
110    solid: &BrepSolid,
111    face_id: u64,
112    distance: f64,
113) -> Result<BrepSolid, KernelRefusal> {
114    if !distance.is_finite() {
115        return Err(KernelRefusal::input(
116            KernelStage::Collect,
117            "distance",
118            "offset_ruled_face: distance must be finite",
119        ));
120    }
121    if let Some(directory) = census_directory() {
122        record_push_census(&directory, solid, face_id, distance);
123    }
124    let scale = solid_model_scale(solid);
125    let tolerance = (scale * 1e-7).max(1e-9);
126    let plane_tolerance = (scale * 1e-6).max(1e-7);
127
128    let (pshell, pface) = find_face(solid, face_id).ok_or_else(|| {
129        KernelRefusal::input(
130            KernelStage::Collect,
131            "face_id",
132            format!("offset_ruled_face: no face {face_id}"),
133        )
134    })?;
135    let pushed = &solid.shells[pshell].faces[pface];
136    let Some(AnalyticSurface::RuledRevolution { frame, .. }) = pushed.surface.analytic() else {
137        return Err(KernelRefusal::input(
138            KernelStage::Collect,
139            "not_ruled",
140            "offset_ruled_face: the pushed face is not a cylinder or cone",
141        ));
142    };
143    let (frame_origin, frame_axis) = (frame.origin, frame.axis);
144
145    // Sign the offset: the carrier grows outward (radius +) along the face's
146    // OUTWARD normal. If that normal points radially inward (a hole wall), a
147    // positive push shrinks the radius.
148    let (mid_point, mid_normal) = outward_normal_mid(pushed)?;
149    let radial = {
150        let d = mid_point.sub(frame_origin);
151        d.sub(frame_axis.scale(d.dot(frame_axis)))
152    };
153    if radial.length() <= tolerance {
154        return Err(KernelRefusal::internal(
155            KernelStage::Classify,
156            "radial_direction",
157            "offset_ruled_face: degenerate radial direction (face on the axis)",
158        ));
159    }
160    let outward_sign = if mid_normal.dot(radial) >= 0.0 { 1.0 } else { -1.0 };
161    let signed_distance = distance * outward_sign;
162
163    let s_prime = offset_ruled_carrier(&pushed.surface, signed_distance, tolerance)?;
164
165    // Edge -> incident faces, to classify each of the pushed face's boundary
166    // edges as a SEAM (both incidences are the pushed face) or a RIM (the other
167    // face is the fixed neighbour cap).
168    let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
169    for shell in &solid.shells {
170        for face in &shell.faces {
171            for loop_record in &face.loops {
172                for coedge in &loop_record.coedges {
173                    faces_of_edge
174                        .entry(coedge.edge_id)
175                        .or_default()
176                        .push(face.id);
177                }
178            }
179        }
180    }
181    let edge_by_id: HashMap<u64, &EdgeRecord> =
182        solid.edges.iter().map(|edge| (edge.id, edge)).collect();
183
184    let mut new_curve: HashMap<u64, NurbsCurve> = HashMap::default();
185    let mut new_vertex: HashMap<u64, Vec3> = HashMap::default();
186    let mut cap_faces: HashSet<u64> = HashSet::default();
187    // Coaxial ruled neighbours (a stepped/telescoping bore or boss: a cylinder
188    // meeting a coaxial cone, or two coaxial cones) — retrimmed on their own
189    // (axially-grown) carriers rather than as planar caps.
190    let mut ruled_faces: HashSet<u64> = HashSet::default();
191    // CURVED neighbours re-intersected through the shared generic lane — a
192    // sphere dome, a fillet torus, a general revolution, a non-coaxial
193    // cylinder. Their carriers do NOT move and need no growth (unlike a plane's
194    // finite patch or a ruled band's axial span, a sphere's and a torus's
195    // domains are already closed over their whole surface, and a re-intersected
196    // rim by construction lands on the carrier it was intersected with), so
197    // they are re-trimmed in place: pcurves rebuilt around the moved rim, and
198    // every OTHER edge they own re-trimmed by parameter on the curve it already
199    // carries — see `retrim_curved_neighbour_ranges`.
200    let mut curved_faces: HashSet<u64> = HashSet::default();
201    let mut seam_edges: Vec<u64> = Vec::new();
202    // Current vertex positions, for rebuilding a ruled neighbour's seam whose
203    // far endpoint (its unmoved rim vertex) is not relocated by any rim.
204    let vertex_pos: HashMap<u64, Vec3> =
205        solid.vertices.iter().map(|v| (v.id, v.point)).collect();
206    // The mouth's TANGENT vertices, where the rim touches a floor that a third
207    // face pinches to a point (a bore inscribed in a hex pocket). Read off
208    // topology before the rim pass, so a non-planar floor refuses by name.
209    let tangents =
210        tangent::tangent_vertices(solid, face_id, &faces_of_edge, &edge_by_id, plane_tolerance)?;
211
212    // OPEN rim arcs (a multi-loop pushed face: a window / slot cut through the
213    // wall). Collected here and trimmed in a second pass, once every corner
214    // vertex has been solved, so both arcs meeting at a corner agree on it.
215    let mut open_rims: Vec<OpenRim> = Vec::new();
216    // Every RIM edge rebuilt below, closed circles and open arcs alike: the
217    // pushed face's pcurves for these are refitted when a multi-loop rebuild
218    // ran, because the replacement conic carries its own parameterization.
219    let mut rim_edges: Vec<u64> = Vec::new();
220
221    // PRE-PASS — a HOLE cut clean through the wall by ONE crossing carrier.
222    //
223    // A window bored by a crossing cylinder, a dome or a torus leaves an
224    // interior loop every one of whose edges is shared with the SAME neighbour
225    // face, and whose corner vertices have valence two: no third face meets
226    // there, so no triple point determines them and `resolve_open_rim_end` —
227    // which solves a triple point against a PLANE — has nothing to solve. The
228    // loop is one closed section that the arrangement stored as arcs, and the
229    // right rebuild is to re-intersect once and cut the section at the samples
230    // nearest the vertices it replaces. Handled here, ahead of the per-edge
231    // loop, because the decision is a property of the whole loop.
232    let mut hole_edges: HashSet<u64> = HashSet::default();
233    for loop_record in &pushed.loops {
234        let Some(hole) = single_neighbour_hole(
235            loop_record,
236            face_id,
237            solid,
238            &faces_of_edge,
239            &edge_by_id,
240            frame_origin,
241            frame_axis,
242            scale,
243        )?
244        else {
245            continue;
246        };
247        rebuild_single_neighbour_hole(
248            solid,
249            &s_prime,
250            &hole,
251            &edge_by_id,
252            &vertex_pos,
253            tolerance,
254            scale,
255            &mut new_curve,
256            &mut new_vertex,
257        )?;
258        for edge_id in &hole.edge_ids {
259            hole_edges.insert(*edge_id);
260            if !rim_edges.contains(edge_id) {
261                rim_edges.push(*edge_id);
262            }
263        }
264        curved_faces.insert(hole.neighbour);
265    }
266
267    for loop_record in &pushed.loops {
268        let coedge_count = loop_record.coedges.len();
269        for coedge_index in 0..coedge_count {
270            let coedge = &loop_record.coedges[coedge_index];
271            let edge = *edge_by_id
272                .get(&coedge.edge_id)
273                .ok_or_else(|| {
274                    KernelRefusal::internal(
275                        KernelStage::Classify,
276                        "missing_edge",
277                        format!("offset_ruled_face: missing edge {}", coedge.edge_id),
278                    )
279                })?;
280            if hole_edges.contains(&edge.id) {
281                continue; // rebuilt whole, by the pre-pass above.
282            }
283            let incident = faces_of_edge.get(&edge.id).cloned().unwrap_or_default();
284            let is_seam = incident.iter().all(|f| *f == face_id);
285            if is_seam {
286                if !seam_edges.contains(&edge.id) {
287                    seam_edges.push(edge.id);
288                }
289                continue;
290            }
291            // RIM: the fixed neighbour is the other face. Three lanes, in
292            // decreasing exactness and increasing generality:
293            //
294            //   * a PLANE (an end cap) or a ruled revolution COAXIAL with the
295            //     pushed carrier (a stepped/telescoping bore or boss) — the two
296            //     original lanes, whose closed forms are reached by the exact
297            //     call this function has always made, unchanged;
298            //   * ANY OTHER carrier — sphere, torus, general revolution,
299            //     non-coaxial cylinder/cone, free-form — through the shared
300            //     re-intersection service (`offset/reintersect.rs`), which tries
301            //     the same closed forms first and marches the pair when they
302            //     decline. This is the lane the audit's §2.3 says push-face
303            //     lacks and offset-shell has.
304            //
305            // The generic lane is admitted only where THIS rebuild can express
306            // the answer: a CLOSED rim, whose single vertex is a bookkeeping
307            // seam point rather than a triple point. An open arc against a
308            // curved neighbour needs `resolve_open_rim_end` to solve a triple
309            // point against a curved `N'`, which it cannot (it takes a plane),
310            // so that stays a refusal with its own reason.
311            let neighbour = *incident
312                .iter()
313                .find(|f| **f != face_id)
314                .ok_or_else(|| {
315                    KernelRefusal::internal(
316                        KernelStage::Classify,
317                        "no_neighbour",
318                        format!("offset_ruled_face: edge {} has no neighbour", edge.id),
319                    )
320                })?;
321            let (nshell, nface) = find_face(solid, neighbour).ok_or_else(|| {
322                KernelRefusal::internal(
323                    KernelStage::Classify,
324                    "missing_neighbour",
325                    format!("offset_ruled_face: missing neighbour {neighbour}"),
326                )
327            })?;
328            let neighbour_surface = &solid.shells[nshell].faces[nface].surface;
329            let is_plane =
330                matches!(neighbour_surface.analytic(), Some(AnalyticSurface::Plane { .. }));
331            let is_coaxial_ruled =
332                ruled_neighbour_is_coaxial(neighbour_surface, frame_origin, frame_axis, scale);
333            let is_curved = !is_plane && !is_coaxial_ruled;
334            let separated = || {
335                KernelRefusal::unsupported(
336                    KernelStage::Classify,
337                    "separated",
338                    "offset_ruled_face: the pushed carrier no longer meets a neighbour \
339                 (the push separated them, or the rim left the neighbour's domain) — refusing",
340                )
341            };
342            // New rim = offset carrier ∩ neighbour carrier, the branch nearest
343            // the old edge.
344            let old_mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).or_refuse(KernelStage::Refine, "evaluate")?;
345            let mut marched = false;
346            let curves = if is_curved {
347                if edge.start_vertex_id != edge.end_vertex_id {
348                    return Err(KernelRefusal::unsupported(
349                        KernelStage::Classify,
350                        "open_curved_rim",
351                        format!(
352                        "offset_ruled_face: the rim against curved neighbour {neighbour} is an \
353                         OPEN arc (edge {}); an arc's corner is a triple point solved against a \
354                         PLANE only — deferred (refusing)",
355                        edge.id
356                    ),
357                    ));
358                }
359                let policy = MarchPolicy {
360                    tolerance,
361                    // The rebuilt rim must sit on BOTH carriers tightly enough
362                    // that its pcurves build and `validate` accepts them; the
363                    // free-form push's own residual gate (0.05% of model scale)
364                    // is the in-tree precedent for "how far an approximate
365                    // offset result may be off".
366                    residual_tolerance: (scale * 5e-4).max(5e-6),
367                    // The boundary being replaced is the best seed set for the
368                    // boundary replacing it.
369                    seeds: edge_seeds(edge, 9)?,
370                };
371                match reintersect_carriers(&s_prime, neighbour_surface, &policy) {
372                    Ok(found) => {
373                        marched = found.lane == RimLane::Marched;
374                        if std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok() {
375                            eprintln!(
376                                "RIM neighbour {neighbour}: lane {:?}, {} branch(es), \
377                                 residual {:.3e} (gate {:.3e})",
378                                found.lane,
379                                found.sections.len(),
380                                found.residual,
381                                policy.residual_tolerance
382                            );
383                        }
384                        found.curves()
385                    }
386                    Err(ReintersectRefusal::Separated) => return Err(separated()),
387                    // A residual past the gate is the march declining its own answer; a
388                    // lower-level failure is a defect. Both keep the text they had.
389                    Err(other @ ReintersectRefusal::Residual { .. }) => {
390                        return Err(KernelRefusal::non_convergence(
391                            KernelStage::Refine,
392                            "reintersect_residual",
393                            format!("offset_ruled_face: {}", other.describe()),
394                        ));
395                    }
396                    Err(other) => {
397                        return Err(KernelRefusal::internal(
398                            KernelStage::Refine,
399                            "reintersect_failed",
400                            format!("offset_ruled_face: {}", other.describe()),
401                        ));
402                    }
403                }
404            } else {
405                // UNCHANGED: the exact call, with the same arguments in the same
406                // order, so every pair this function answered before is answered
407                // bit-identically now.
408                intersect_analytic_pair(&s_prime, neighbour_surface, tolerance)
409                    .filter(|curves| !curves.is_empty())
410                    .ok_or_else(separated)?
411            };
412            let rim = nearest_curve(&curves, old_mid)?;
413            if !rim_edges.contains(&edge.id) {
414                rim_edges.push(edge.id);
415            }
416            if edge.start_vertex_id == edge.end_vertex_id {
417                // A closed rim is a whole conic, so it must also run the way the
418                // edge it replaces ran — see `match_closed_rim_direction`.
419                let rim = if marched {
420                    // A MARCHED section begins wherever the trace was seeded,
421                    // not on the carrier's seam, so the start-tangent test would
422                    // compare two unrelated places. Ask the same question at the
423                    // old curve's nearest parameter instead.
424                    match_marched_rim_direction(rim, edge)?
425                } else {
426                    match_closed_rim_direction(rim, edge)?
427                };
428                // CLOSED rim (the canonical cap circle of a single-loop push):
429                // its seam-azimuth point is the relocated corner and matches the
430                // offset carrier's meridian. A recognized carrier's u = 0 IS its
431                // seam, and the shared rim's one seam vertex sits on both
432                // carriers' seams, so this placement keeps the coaxial
433                // neighbour's straight meridian on its carrier too. A marched
434                // section has no such convention, so its own domain start is
435                // where its single vertex goes — the vertex is a bookkeeping
436                // split of a closed curve, not a geometric corner.
437                let seam_point = if marched {
438                    let [d0, _] = rim.domain().or_refuse(KernelStage::Refine, "domain")?;
439                    rim.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?
440                } else {
441                    rim.evaluate(0.0).or_refuse(KernelStage::Refine, "evaluate")?
442                };
443                new_vertex.insert(edge.start_vertex_id, seam_point);
444                new_vertex.insert(edge.end_vertex_id, seam_point);
445                new_curve.insert(edge.id, rim);
446            } else {
447                // OPEN rim: an arc / generatrix bounding a window cut through
448                // the wall. `rim` is the WHOLE conic the two carriers share, so
449                // each endpoint must be re-solved (it is the triple point
450                // `S' ∩ N ∩ N'` against the ADJACENT rim's neighbour, or the
451                // pushed carrier's own seam) and the conic trimmed between them.
452                // Collapsing both onto `rim.evaluate(0.0)` — the closed-rim rule
453                // — is what used to tear a multi-loop push apart.
454                let previous =
455                    &loop_record.coedges[(coedge_index + coedge_count - 1) % coedge_count];
456                let next = &loop_record.coedges[(coedge_index + 1) % coedge_count];
457                let (at_start, at_end) = if coedge.forward {
458                    (previous, next)
459                } else {
460                    (next, previous)
461                };
462                let mut resolve = |adjacent_coedge: &CoedgeRecord,
463                                   vertex_id: u64|
464                 -> Result<RimEnd, KernelRefusal> {
465                    let adjacent = *edge_by_id.get(&adjacent_coedge.edge_id).ok_or_else(|| {
466                        KernelRefusal::internal(
467                            KernelStage::Refine,
468                            "adjacent_edge",
469                            format!(
470                            "offset_ruled_face: missing edge {}",
471                            adjacent_coedge.edge_id
472                        ),
473                        )
474                    })?;
475                    let old_point = vertex_pos
476                        .get(&vertex_id)
477                        .copied()
478                        .ok_or_else(|| {
479                            KernelRefusal::internal(
480                                KernelStage::Refine,
481                                "corner_vertex_pos",
482                                format!("offset_ruled_face: missing vertex {vertex_id}"),
483                            )
484                        })?;
485                    let (end, point) = resolve_open_rim_end(
486                        solid,
487                        face_id,
488                        &faces_of_edge,
489                        &rim,
490                        adjacent,
491                        old_point,
492                        plane_tolerance,
493                    )?;
494                    // Both arcs meeting at a corner solve the SAME triple point
495                    // from their own conic; a disagreement means the branch pick
496                    // went to different roots, so refuse rather than tear.
497                    match new_vertex.get(&vertex_id).copied() {
498                        Some(existing) if existing.sub(point).length() > plane_tolerance => {
499                            return Err(KernelRefusal::internal(
500                                KernelStage::Refine,
501                                "corner_agreement",
502                                "offset_ruled_face: the two rims meeting at a multi-loop corner \
503                                 disagree on its new position — refusing",
504                            ))
505                        }
506                        Some(_) => {}
507                        None => {
508                            new_vertex.insert(vertex_id, point);
509                        }
510                    }
511                    Ok(end)
512                };
513                let start = resolve(at_start, edge.start_vertex_id)?;
514                let end = resolve(at_end, edge.end_vertex_id)?;
515                open_rims.push(OpenRim {
516                    edge_id: edge.id,
517                    conic: rim,
518                    start,
519                    end,
520                    old_mid,
521                    start_vertex_id: edge.start_vertex_id,
522                    end_vertex_id: edge.end_vertex_id,
523                });
524            }
525            if is_plane {
526                cap_faces.insert(neighbour);
527            } else if is_coaxial_ruled {
528                ruled_faces.insert(neighbour);
529            } else {
530                curved_faces.insert(neighbour);
531            }
532        }
533    }
534
535    // Second pass: trim each open rim's conic to the arc BETWEEN its two solved
536    // endpoints — the one that CONTAINS the old edge, picked by the old
537    // midpoint's parameter — and orient it start-vertex → end-vertex.
538    //
539    // Both endpoints are parameters on the same conic (a seam end is solved on
540    // the seam meridian, see `RimEnd::Seam`), so there is one rule for every
541    // arc: the wanted piece is `[low, high]` when the midpoint lies in it, and
542    // the COMPLEMENT — the piece that runs the other way round, across the
543    // conic's period boundary — when it does not. The complement used to be an
544    // unconditional refusal; it is an ordinary arc whenever one of its two
545    // halves is empty, which is exactly the case of an endpoint that landed ON
546    // the period boundary. The reporter's gear bore has two of them: its top
547    // rim is split at the carrier's own seam azimuth, so the vertex there
548    // solves to parameter 0 while the arc it starts is `[0.9946, 1]`.
549    for rim in &open_rims {
550        let [d0, d1] = rim.conic.domain().or_refuse(KernelStage::Fragment, "domain")?;
551        let middle = project_point_to_curve(&rim.conic, rim.old_mid).or_refuse(KernelStage::Fragment, "project_point_to_curve")?.u;
552        let (a, b) = (rim.start.parameter(), rim.end.parameter());
553        let (low, high) = if a <= b { (a, b) } else { (b, a) };
554        if std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok() {
555            let describe = |end: RimEnd| match end {
556                RimEnd::Corner(u) => format!("Corner({u:.9})"),
557                RimEnd::Seam(u) => format!("Seam({u:.9})"),
558            };
559            let at = |vid: u64| {
560                new_vertex
561                    .get(&vid)
562                    .map(|p: &Vec3| format!("({:.5},{:.5},{:.5})", p.x, p.y, p.z))
563                    .unwrap_or_else(|| "-".to_string())
564            };
565            eprintln!(
566                "OPENRIM edge {} domain=[{d0},{d1}] start={} end={} middle={middle:.9} \
567                 v{}{} -> v{}{}",
568                rim.edge_id,
569                describe(rim.start),
570                describe(rim.end),
571                rim.start_vertex_id,
572                at(rim.start_vertex_id),
573                rim.end_vertex_id,
574                at(rim.end_vertex_id),
575            );
576        }
577        if let (RimEnd::Seam(_), RimEnd::Seam(_)) = (rim.start, rim.end) {
578            return Err(KernelRefusal::unsupported(
579                KernelStage::Fragment,
580                "seam_both_ends",
581                "offset_ruled_face: a multi-loop rim arc ends on the seam at BOTH ends — \
582                 refusing",
583            ));
584        }
585        let mut trimmed = rim_arc_between(&rim.conic, low, high, middle, tolerance)?;
586        let start_point = *new_vertex.get(&rim.start_vertex_id).ok_or_else(|| {
587            KernelRefusal::internal(
588                KernelStage::Fragment,
589                "corner_start_unrelocated",
590                "offset_ruled_face: a multi-loop rim corner was not relocated — refusing",
591            )
592        })?;
593        let end_point = *new_vertex.get(&rim.end_vertex_id).ok_or_else(|| {
594            KernelRefusal::internal(
595                KernelStage::Fragment,
596                "corner_end_unrelocated",
597                "offset_ruled_face: a multi-loop rim corner was not relocated — refusing",
598            )
599        })?;
600        let [t0, _] = trimmed.domain().or_refuse(KernelStage::Fragment, "domain")?;
601        let head = trimmed.evaluate(t0).or_refuse(KernelStage::Fragment, "evaluate")?;
602        if head.sub(start_point).length() > head.sub(end_point).length() {
603            trimmed = trimmed.reversed().or_refuse(KernelStage::Fragment, "reversed")?;
604        }
605        new_curve.insert(rim.edge_id, trimmed);
606    }
607
608    // A tangent vertex cannot follow the rim — its flat stays — so it is split:
609    // the plan gets the new rim vertex, and a bridge edge or a merged floor.
610    // With no tangent vertex the plan stays geometry-only.
611    let mut plan = RebuildPlan::default();
612    tangent::plan_tangent_vertices(
613        solid,
614        face_id,
615        &tangents,
616        &faces_of_edge,
617        &edge_by_id,
618        &vertex_pos,
619        &s_prime,
620        plane_tolerance,
621        &mut new_vertex,
622        &mut cap_faces,
623        &mut plan,
624    )?;
625
626    let pos = |vid: u64| -> Vec3 {
627        new_vertex
628            .get(&vid)
629            .copied()
630            .unwrap_or_else(|| vertex_pos[&vid])
631    };
632
633    // Seam edge(s): the straight generatrix segment of the offset carrier, from
634    // the relocated bottom seam vertex to the top one. Both endpoints were placed
635    // on S′'s seam meridian by the rim re-intersections above, so a line between
636    // them rides u = 0 of S′ over the SAME axial range the seam had — using the
637    // full `iso_curve_u` meridian would wrongly span the untrimmed carrier (a
638    // boolean-drilled wall's surface runs past the plate faces).
639    for seam_id in &seam_edges {
640        let seam = *edge_by_id.get(seam_id).ok_or_else(|| {
641            KernelRefusal::internal(
642                KernelStage::Fragment,
643                "missing_seam_edge",
644                format!("offset_ruled_face: missing seam edge {seam_id}"),
645            )
646        })?;
647        let start = *new_vertex.get(&seam.start_vertex_id).ok_or_else(|| {
648            KernelRefusal::internal(
649                KernelStage::Fragment,
650                "seam_start_unrelocated",
651                "offset_ruled_face: seam endpoint was not relocated by a rim — refusing",
652            )
653        })?;
654        let end = *new_vertex.get(&seam.end_vertex_id).ok_or_else(|| {
655            KernelRefusal::internal(
656                KernelStage::Fragment,
657                "seam_end_unrelocated",
658                "offset_ruled_face: seam endpoint was not relocated by a rim — refusing",
659            )
660        })?;
661        new_curve.insert(*seam_id, make_line(start, end).or_refuse(KernelStage::Fragment, "make_line")?);
662    }
663
664    // A COAXIAL ruled neighbour also has its own straight seam meridian ending
665    // at the shared rim's (now moved) seam vertex. Rebuild it as the chord
666    // between its endpoints — one relocated by the rim, the other its unmoved
667    // rim vertex — which, both lying on the neighbour's straight generatrix, is
668    // exactly the meridian segment.
669    for ruled in &ruled_faces {
670        let (nshell, nface) = find_face(solid, *ruled).ok_or_else(|| {
671            KernelRefusal::internal(
672                KernelStage::Fragment,
673                "missing_ruled_neighbour",
674                format!("offset_ruled_face: missing ruled neighbour {ruled}"),
675            )
676        })?;
677        for loop_record in &solid.shells[nshell].faces[nface].loops {
678            for coedge in &loop_record.coedges {
679                let edge = *edge_by_id.get(&coedge.edge_id).ok_or_else(|| {
680                    KernelRefusal::internal(
681                        KernelStage::Fragment,
682                        "ruled_seam_edge",
683                        format!("offset_ruled_face: missing edge {}", coedge.edge_id),
684                    )
685                })?;
686                let incident = faces_of_edge.get(&edge.id).cloned().unwrap_or_default();
687                let is_seam = incident.iter().all(|f| *f == *ruled);
688                let touches_moved = new_vertex.contains_key(&edge.start_vertex_id)
689                    || new_vertex.contains_key(&edge.end_vertex_id);
690                if is_seam && touches_moved && !new_curve.contains_key(&edge.id) {
691                    new_curve.insert(
692                        edge.id,
693                        make_line(pos(edge.start_vertex_id), pos(edge.end_vertex_id)).or_refuse(KernelStage::Fragment, "make_line")?,
694                    );
695                }
696            }
697        }
698    }
699
700    // A CURVED neighbour keeps its surface, so it keeps every 3D CURVE it owns
701    // — a sphere's seam meridian is the same great-circle arc after the push as
702    // before it. What changes is where that curve is TRIMMED: the endpoint the
703    // moved rim carried with it slides along the curve to a new parameter.
704    // Re-solving it as a parameter on the existing curve is EXACT, and it is
705    // strictly better than rebuilding: the coaxial-ruled lane above can chord
706    // its seam only because a ruled revolution's meridian is straight, and a
707    // sphere's is not.
708    let mut new_range: HashMap<u64, (f64, f64)> = HashMap::default();
709    for curved in &curved_faces {
710        let (nshell, nface) = find_face(solid, *curved).ok_or_else(|| {
711            KernelRefusal::internal(
712                KernelStage::Fragment,
713                "missing_curved_neighbour",
714                format!("offset_ruled_face: missing curved neighbour {curved}"),
715            )
716        })?;
717        for loop_record in &solid.shells[nshell].faces[nface].loops {
718            for coedge in &loop_record.coedges {
719                let edge = *edge_by_id.get(&coedge.edge_id).ok_or_else(|| {
720                    KernelRefusal::internal(
721                        KernelStage::Fragment,
722                        "curved_neighbour_edge",
723                        format!("offset_ruled_face: missing edge {}", coedge.edge_id),
724                    )
725                })?;
726                if new_curve.contains_key(&edge.id) || new_range.contains_key(&edge.id) {
727                    continue;
728                }
729                let start_moved = new_vertex.get(&edge.start_vertex_id).copied();
730                let end_moved = new_vertex.get(&edge.end_vertex_id).copied();
731                if start_moved.is_none() && end_moved.is_none() {
732                    continue;
733                }
734                if edge.start_vertex_id == edge.end_vertex_id {
735                    // A closed or degenerate edge of the neighbour (its own
736                    // opposite rim, or a pole) whose vertex a rim relocated:
737                    // the whole curve would have to move, which this lane does
738                    // not do.
739                    return Err(KernelRefusal::unsupported(
740                        KernelStage::Fragment,
741                        "closed_neighbour_edge",
742                        format!(
743                        "offset_ruled_face: the push moved the vertex of curved neighbour \
744                         {curved}'s CLOSED edge {} — deferred (refusing)",
745                        edge.id
746                    ),
747                    ));
748                }
749                let mut range = (edge.t0, edge.t1);
750                for (moved, slot) in [(start_moved, 0usize), (end_moved, 1usize)] {
751                    let Some(point) = moved else { continue };
752                    let projection = project_point_to_curve(&edge.curve, point).or_refuse(KernelStage::Fragment, "project_point_to_curve")?;
753                    if projection.distance > plane_tolerance {
754                        return Err(KernelRefusal::unsupported(
755                            KernelStage::Fragment,
756                            "vertex_off_neighbour_edge",
757                            format!(
758                            "offset_ruled_face: a relocated rim vertex left curved neighbour \
759                             {curved}'s edge {} (off by {:.3e}) — refusing",
760                            edge.id, projection.distance
761                        ),
762                        ));
763                    }
764                    if slot == 0 {
765                        range.0 = projection.u;
766                    } else {
767                        range.1 = projection.u;
768                    }
769                }
770                new_range.insert(edge.id, range);
771            }
772        }
773    }
774    // Fail-safe: every relocated vertex must belong only to edges this push
775    // rebuilt or re-trimmed. A vertex that also ends an edge of some other face
776    // would leave that face's boundary behind, which is exactly the silent
777    // tear the ruled lane's own corner guard refuses.
778    if !curved_faces.is_empty() {
779        for edge in &solid.edges {
780            if new_curve.contains_key(&edge.id) || new_range.contains_key(&edge.id) {
781                continue;
782            }
783            if new_vertex.contains_key(&edge.start_vertex_id)
784                || new_vertex.contains_key(&edge.end_vertex_id)
785            {
786                return Err(KernelRefusal::unsupported(
787                    KernelStage::Fragment,
788                    "third_edge_moved",
789                    format!(
790                    "offset_ruled_face: relocating a curved neighbour's rim moved the end of \
791                     edge {}, which this push does not rebuild — refusing",
792                    edge.id
793                ),
794                ));
795            }
796        }
797    }
798
799    // A multi-loop window's corner is a TRIPLE point, so it is also the end of a
800    // third edge that belongs to neither the pushed face nor a rim: the two
801    // fixed neighbour planes' own shared edge (a slot floor meeting a slot
802    // side wall). Both carriers are fixed, so that edge stays on their
803    // intersection line and the chord between its (possibly relocated)
804    // endpoints IS the rebuilt edge. Skipped entirely when no open rim was
805    // rebuilt, so the single-loop paths are untouched.
806    if !open_rims.is_empty() {
807        let mut corner_edges: Vec<(u64, NurbsCurve)> = Vec::new();
808        for edge in &solid.edges {
809            if new_curve.contains_key(&edge.id) {
810                continue;
811            }
812            if !new_vertex.contains_key(&edge.start_vertex_id)
813                && !new_vertex.contains_key(&edge.end_vertex_id)
814            {
815                continue;
816            }
817            if edge.curve.straight_segment(tolerance).is_none() {
818                return Err(KernelRefusal::unsupported(
819                    KernelStage::Fragment,
820                    "curved_third_edge",
821                    "offset_ruled_face: the push moved the end of a CURVED edge that is not a \
822                     rebuilt rim — refusing",
823                ));
824            }
825            let start = pos(edge.start_vertex_id);
826            let end = pos(edge.end_vertex_id);
827            for neighbour in faces_of_edge.get(&edge.id).cloned().unwrap_or_default() {
828                if !cap_faces.contains(&neighbour) {
829                    // Name the vertex, the edge and the face. This is the
830                    // refusal a rim corner PINNED by a third, fixed face gives
831                    // — the reporter's gear bore is inscribed in a hex pocket,
832                    // so each of its six tangent corners also ends the bottom
833                    // edge of a hex flat, and moving the rim off those flats is
834                    // a topology change (the six floor faces merge) that push
835                    // face does not perform. Without the ids the message says
836                    // nothing about which corner is stuck.
837                    let stuck = [edge.start_vertex_id, edge.end_vertex_id]
838                        .into_iter()
839                        .find(|vertex| new_vertex.contains_key(vertex))
840                        .unwrap_or(edge.start_vertex_id);
841                    return Err(KernelRefusal::unsupported(
842                        KernelStage::Fragment,
843                        "pinned_corner",
844                        format!(
845                        "offset_ruled_face: the push relocates rim corner vertex {stuck}, which \
846                         also ends edge {} of face {neighbour} — a face this push does not \
847                         re-trim, so the rim would tear away from it (refusing)",
848                        edge.id
849                    ),
850                    ));
851                }
852                let (nshell, nface) = find_face(solid, neighbour).ok_or_else(|| {
853                    KernelRefusal::internal(
854                        KernelStage::Fragment,
855                        "corner_neighbour",
856                        format!("offset_ruled_face: missing neighbour {neighbour}"),
857                    )
858                })?;
859                let plane = plane_of_surface(
860                    &solid.shells[nshell].faces[nface].surface,
861                    plane_tolerance,
862                    "offset_ruled_face",
863                )
864                ?;
865                for point in [start, end] {
866                    if point.sub(plane.origin).dot(plane.normal).abs() > plane_tolerance {
867                        return Err(KernelRefusal::unsupported(
868                            KernelStage::Fragment,
869                            "corner_off_plane",
870                            "offset_ruled_face: a relocated corner left one of its fixed \
871                             neighbour planes — refusing",
872                        ));
873                    }
874                }
875            }
876            corner_edges.push((edge.id, make_line(start, end).or_refuse(KernelStage::Fragment, "make_line")?));
877        }
878        for (edge_id, curve) in corner_edges {
879            new_curve.insert(edge_id, curve);
880        }
881    }
882
883    // --- Apply the plan to a fresh clone (the input is never mutated) -------
884    //
885    // Everything above is the classification step; what it decided is the plan.
886    plan.curves = new_curve;
887    plan.ranges = new_range;
888    plan.moved = new_vertex;
889    let (mut result, applied) = plan.apply(solid)?;
890    // Every edge this push touched, by curve, by trim range or by being added —
891    // the selective re-fit's work list.
892    let changed_edges = plan.changed_edges(&applied);
893    let (pshell, pface) = find_face(&result, face_id).ok_or_else(|| {
894        KernelRefusal::internal(
895            KernelStage::Fragment,
896            "pushed_face_lookup",
897            format!("offset_ruled_face: missing face {face_id}"),
898        )
899    })?;
900    // The pushed face rides the offset carrier. S′ shares the source's parameter
901    // domain + seam azimuth (same make_revolution frame/span), so the face's
902    // existing (u, v) pcurves stay valid when every rim stayed at its axial
903    // station (the planar-cap push) — only the surface swaps.
904    result.shells[pshell].faces[pface].surface = s_prime;
905
906    let final_edges: HashMap<u64, EdgeRecord> =
907        result.edges.iter().map(|e| (e.id, e.clone())).collect();
908
909    // A COAXIAL ruled neighbour moves the shared rim ALONG the axis, so the
910    // pushed face's pcurve for that rim (and the seam whose endpoint moved) is
911    // no longer at its old v — refit both the pushed face and every ruled
912    // neighbour on their (axially-grown-if-needed) carriers. The planar-only
913    // push skips this and keeps the exact pcurve reuse above.
914    //
915    // A CURVED neighbour moves the shared rim just as much (a dome's rim climbs
916    // the sphere as the rod it caps grows), so it joins the same pass — but its
917    // own carrier neither moves nor grows: a sphere, a torus and a full
918    // revolution are already closed over their whole surface, and the rim was
919    // intersected WITH that surface, so it is on it by construction. Its retrim
920    // is therefore the same driver with a growth that does nothing.
921    if !ruled_faces.is_empty() {
922        retrim_offset_ruled_face(&mut result, face_id, &final_edges, tolerance)?;
923        for ruled in &ruled_faces {
924            retrim_offset_ruled_face(&mut result, *ruled, &final_edges, tolerance)?;
925        }
926        for curved in &curved_faces {
927            refit_changed_pcurves(&mut result, *curved, &final_edges, &changed_edges, tolerance)?;
928        }
929    } else if !curved_faces.is_empty() {
930        // The pushed carrier still has to GROW where the rim climbed past its
931        // axial span — the same exact prolongation the ruled lane uses — but its
932        // pcurves are re-fitted selectively, not wholesale.
933        let (gshell, gface) = find_face(&result, face_id).ok_or_else(|| {
934            KernelRefusal::internal(
935                KernelStage::Sew,
936                "grow_face_lookup",
937                format!("offset_ruled_face: missing face {face_id}"),
938            )
939        })?;
940        let samples = boundary_samples(
941            &result.shells[gshell].faces[gface],
942            &final_edges,
943            "offset_ruled_face",
944        )?;
945        extend_ruled_neighbour_over(&mut result, face_id, &samples, tolerance)?;
946        refit_changed_pcurves(&mut result, face_id, &final_edges, &changed_edges, tolerance)?;
947        for curved in &curved_faces {
948            refit_changed_pcurves(&mut result, *curved, &final_edges, &changed_edges, tolerance)?;
949        }
950    } else if !rim_edges.is_empty() {
951        // Every rebuilt RIM needs a fresh pcurve on S'. An open rim because its
952        // azimuth span (an arc) or station (a generatrix) genuinely moved; a
953        // CLOSED rim because the replacement conic carries the INTERSECTOR's
954        // parameterization, which need not be the one the incoming curve had —
955        // a boolean-cut cylinder's cap circle comes back re-parameterized, and
956        // a closed rim under an OBLIQUE planar cap is an ellipse whose height
957        // profile v(u) changes with the offset (the cap plane climbs as the
958        // carrier grows: measured 0.382 against a 0.206 limit when the old
959        // pcurve was kept on the oblique-cut cone of PushFaceTest2). So this
960        // pass runs for every rebuilt rim, not only when an open rim exists;
961        // for a perpendicular cap circle it rebuilds the same v = const line.
962        // `validate` pairs pcurve to curve by FRACTION of their domains, so a
963        // pointwise-identical circle with a different parameter distribution
964        // still reads as a gross deviation (measured 5.96 and 12.0 on the slot
965        // fixture, against a 0.394 limit).
966        //
967        // The SEAM edges keep their u — and with it the two coedges sitting on
968        // OPPOSITE sides of the periodic seam — but their v is REFRESHED, which
969        // is where this used to be wrong.
970        //
971        // The claim that stood here was that a seam is "rebuilt as a straight
972        // chord over the same axial range, so fraction ↦ height is unchanged".
973        // That is true of a cap PERPENDICULAR to the axis and false of an
974        // OBLIQUE one: a tilted plane meets the generatrix at azimuth `u` at an
975        // axial station that depends on the RADIUS, so changing the radius by
976        // `d` slides the seam's endpoint along the axis by `d·tan θ`, where `θ`
977        // is the angle between the axis and the cap's normal. Measured on a
978        // radius-3 bore drilled at 30° and pushed by 0.5: the seam's pcurve sat
979        // `2.887e-1` off its own edge — exactly `0.5·tan 30°` — and
980        // `validate()`'s scale-derived pcurve limit passed it, so the only
981        // symptom was the body's volume, `1.9e-7` relative against its closed
982        // form where the axis-aligned control reads `1.6e-13`.
983        let rebuilt: HashSet<u64> = rim_edges.iter().copied().collect();
984        let seams: HashSet<u64> = seam_edges.iter().copied().collect();
985        let surface = result.shells[pshell].faces[pface].surface.clone();
986        let [u_start, u_end] = surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
987        let u_period = u_end - u_start;
988        for loop_record in &mut result.shells[pshell].faces[pface].loops {
989            for coedge in &mut loop_record.coedges {
990                if !rebuilt.contains(&coedge.edge_id) {
991                    continue;
992                }
993                let edge = final_edges.get(&coedge.edge_id).ok_or_else(|| {
994                    KernelRefusal::internal(
995                        KernelStage::Sew,
996                        "rim_pcurve_edge",
997                        format!("offset_ruled_face: missing edge {}", coedge.edge_id),
998                    )
999                })?;
1000                let mut pcurve = build_pcurve_on_surface(&surface, &edge.curve)
1001                    .or_refuse(KernelStage::Sew, "build_pcurve_on_surface")?;
1002                if !coedge.forward {
1003                    pcurve = pcurve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
1004                }
1005                coedge.pcurve = reanchor_pcurve_u(&pcurve, &coedge.pcurve, u_period)?;
1006            }
1007        }
1008        // The seam pass, in the same loop shape but keeping u: a generatrix's
1009        // azimuth does not change under a radial offset, only the station its
1010        // ends sit at.
1011        for loop_record in &mut result.shells[pshell].faces[pface].loops {
1012            for coedge in &mut loop_record.coedges {
1013                if !seams.contains(&coedge.edge_id) {
1014                    continue;
1015                }
1016                let edge = final_edges.get(&coedge.edge_id).ok_or_else(|| {
1017                    KernelRefusal::internal(
1018                        KernelStage::Sew,
1019                        "seam_pcurve_edge",
1020                        format!("offset_ruled_face: missing edge {}", coedge.edge_id),
1021                    )
1022                })?;
1023                coedge.pcurve = reseat_seam_pcurve(&surface, &coedge.pcurve, edge, coedge.forward)?;
1024            }
1025        }
1026    }
1027
1028    // The fixed caps re-trim as planar faces around their grown rim circles.
1029    for cap in &cap_faces {
1030        let (cshell, cface) = find_face(&result, *cap).ok_or_else(|| {
1031            KernelRefusal::internal(
1032                KernelStage::Sew,
1033                "missing_cap",
1034                format!("offset_ruled_face: missing cap {cap}"),
1035            )
1036        })?;
1037        let plane = plane_of_surface(
1038            &result.shells[cshell].faces[cface].surface,
1039            plane_tolerance,
1040            "offset_ruled_face",
1041        )?;
1042        retrim_planar_face(
1043            &mut result.shells[cshell].faces[cface],
1044            &plane,
1045            &final_edges,
1046            scale,
1047            "offset_ruled_face",
1048        )?;
1049    }
1050
1051    let issues = result.validate();
1052    if !issues.is_empty() {
1053        if std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok() {
1054            // Which coedge of the pushed face drifted, and by how much, with
1055            // the pcurve's endpoints so a re-anchoring or direction mistake is
1056            // visible next to a fitting one.
1057            let (pshell, pface) = find_face(&result, face_id).expect("pushed face survives");
1058            let face = &result.shells[pshell].faces[pface];
1059            for (li, loop_record) in face.loops.iter().enumerate() {
1060                for coedge in &loop_record.coedges {
1061                    let Some(edge) = result.edges.iter().find(|e| e.id == coedge.edge_id) else {
1062                        continue;
1063                    };
1064                    let (Ok([p0, p1]), Ok(c0)) = (coedge.pcurve.domain(), edge.curve.evaluate(edge.t0))
1065                    else {
1066                        continue;
1067                    };
1068                    let mut worst = 0.0f64;
1069                    for k in 0..=32 {
1070                        let f = k as f64 / 32.0;
1071                        let t = if coedge.forward {
1072                            edge.t0 + (edge.t1 - edge.t0) * f
1073                        } else {
1074                            edge.t1 - (edge.t1 - edge.t0) * f
1075                        };
1076                        if let (Ok(q), Ok(target)) =
1077                            (coedge.pcurve.evaluate(p0 + (p1 - p0) * f), edge.curve.evaluate(t))
1078                        {
1079                            if let Ok(on_s) = face.surface.evaluate(q.x, q.y) {
1080                                worst = worst.max(on_s.sub(target).length());
1081                            }
1082                        }
1083                    }
1084                    let (Ok(a), Ok(b)) = (coedge.pcurve.evaluate(p0), coedge.pcurve.evaluate(p1)) else {
1085                        continue;
1086                    };
1087                    eprintln!(
1088                        "PUSH-DEBUG face {} loop {li} edge {} fwd={} rebuilt={} t=[{:.4},{:.4}] dom={:?} \
1089                         curve(t0)=({:.4},{:.4},{:.4}) pcurve ({:.4},{:.4})->({:.4},{:.4}) dev={worst:.4}",
1090                        face.id,
1091                        edge.id,
1092                        coedge.forward,
1093                        rim_edges.contains(&edge.id),
1094                        edge.t0,
1095                        edge.t1,
1096                        edge.curve.domain().ok(),
1097                        c0.x,
1098                        c0.y,
1099                        c0.z,
1100                        a.x,
1101                        a.y,
1102                        b.x,
1103                        b.y
1104                    );
1105                }
1106            }
1107        }
1108        return Err(KernelRefusal::internal(
1109            KernelStage::Validate,
1110            "validate",
1111            format!(
1112            "offset_ruled_face: pushed solid failed validation: {issues:?}"
1113        ),
1114        ));
1115    }
1116    if let (Ok(before), Ok(after)) =
1117        (solid_signed_volume(solid), solid_signed_volume(&result))
1118    {
1119        if before * after <= 0.0 {
1120            return Err(KernelRefusal::internal(
1121                KernelStage::Validate,
1122                INVERTED,
1123                "offset_ruled_face: the push inverts the solid — refusing",
1124            ));
1125        }
1126    }
1127    Ok(result)
1128}
1129
1130/// Put a SEAM meridian's pcurve back on its own (rebuilt) chord without moving
1131/// it off the periodic seam: the u coordinates are carried over verbatim from
1132/// the pcurve being replaced — they are what put the two coedges of a seam on
1133/// OPPOSITE sides of the period — and only v is re-solved.
1134///
1135/// `v` is solved exactly rather than fitted. A ruled revolution is affine in
1136/// `v` along a fixed `u`, so the station of a point on that generatrix is its
1137/// projection onto the segment the v-domain spans, and two surface evaluations
1138/// give the whole map.
1139fn reseat_seam_pcurve(
1140    surface: &NurbsSurface,
1141    pcurve: &NurbsCurve,
1142    edge: &EdgeRecord,
1143    forward: bool,
1144) -> Result<NurbsCurve, KernelRefusal> {
1145    let [q0, q1] = pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
1146    let (head, tail) = (pcurve.evaluate(q0).or_refuse(KernelStage::Sew, "evaluate")?, pcurve.evaluate(q1).or_refuse(KernelStage::Sew, "evaluate")?);
1147    let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Sew, "domain_v")?;
1148    let station = |u: f64, target: Vec3| -> Result<f64, KernelRefusal> {
1149        let base = surface.evaluate(u, v0).or_refuse(KernelStage::Sew, "evaluate")?;
1150        let top = surface.evaluate(u, v1).or_refuse(KernelStage::Sew, "evaluate")?;
1151        let direction = top.sub(base);
1152        let length_squared = direction.length_squared();
1153        if length_squared <= 0.0 {
1154            return Err(KernelRefusal::internal(
1155                KernelStage::Sew,
1156                "generatrix_length",
1157                "offset_ruled_face: the pushed carrier's generatrix has zero length — refusing",
1158            ));
1159        }
1160        Ok(v0 + (v1 - v0) * (target.sub(base).dot(direction) / length_squared))
1161    };
1162    let (at_head, at_tail) = if forward {
1163        (edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?, edge.curve.evaluate(edge.t1).or_refuse(KernelStage::Sew, "evaluate")?)
1164    } else {
1165        (edge.curve.evaluate(edge.t1).or_refuse(KernelStage::Sew, "evaluate")?, edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?)
1166    };
1167    let reseated = crate::make_line(
1168        Vec3::new(head.x, station(head.x, at_head)?, 0.0),
1169        Vec3::new(tail.x, station(tail.x, at_tail)?, 0.0),
1170    ).or_refuse(KernelStage::Sew, "make_line")?;
1171    // Checked, not asserted: the reseated pcurve must land on the chord it
1172    // carries, at both ends and in the middle.
1173    let [r0, r1] = reseated.domain().or_refuse(KernelStage::Sew, "domain")?;
1174    for (parameter, target) in [(r0, at_head), (r1, at_tail)] {
1175        let uv = reseated.evaluate(parameter).or_refuse(KernelStage::Sew, "evaluate")?;
1176        let drift = surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Sew, "evaluate")?.sub(target).length();
1177        if drift > 1e-6 * (1.0 + target.length()) {
1178            return Err(KernelRefusal::internal(
1179                KernelStage::Sew,
1180                "seam_reseat",
1181                format!(
1182                "offset_ruled_face: the reseated seam pcurve misses its own chord by \
1183                 {drift:.3e} — refusing"
1184            ),
1185            ));
1186        }
1187    }
1188    Ok(reseated)
1189}
1190
1191/// Where an OPEN rim arc's endpoint sits on the rebuilt conic — in every case
1192/// a PARAMETER on that conic, solved from geometry.
1193#[derive(Clone, Copy)]
1194enum RimEnd {
1195    /// A genuine corner: the TRIPLE point `offset carrier ∩ this rim's
1196    /// neighbour ∩ the ADJACENT rim's neighbour`, carried as the conic
1197    /// parameter that lands on it.
1198    Corner(f64),
1199    /// The pushed carrier's periodic SEAM split this rim in two, so the
1200    /// endpoint rides the offset carrier's seam MERIDIAN — the parameter at
1201    /// which the conic crosses that meridian's axial half-plane.
1202    ///
1203    /// It is NOT parameter 0 of the conic. `S'` is revolved about the same
1204    /// frame as the source carrier, so parameter 0 sits at `frame.x_axis`'s
1205    /// azimuth — and a face's topological seam edge need not be there. An
1206    /// imported cylinder can carry its seam edge anywhere on the carrier: the
1207    /// reporter's gear bore (`IMPORT3D1_Face_29`) has its seam edge at
1208    /// u = 0.5, a full 180° from `frame.x_axis`, and reading it as parameter 0
1209    /// relocated both seam vertices to the wrong meridian and trimmed each
1210    /// abutting arc from `[0, corner]` instead of `[0.5, corner]`.
1211    Seam(f64),
1212}
1213
1214impl RimEnd {
1215    /// The conic parameter this endpoint lands on.
1216    fn parameter(self) -> f64 {
1217        match self {
1218            RimEnd::Corner(parameter) | RimEnd::Seam(parameter) => parameter,
1219        }
1220    }
1221}
1222
1223/// One open rim arc, held between the two passes of the rebuild.
1224struct OpenRim {
1225    edge_id: u64,
1226    /// The WHOLE conic `S' ∩ N`, before trimming.
1227    conic: NurbsCurve,
1228    start: RimEnd,
1229    end: RimEnd,
1230    /// The OLD edge's midpoint: picks which of the two complementary arcs
1231    /// between the corners is the one this edge actually was.
1232    old_mid: Vec3,
1233    start_vertex_id: u64,
1234    end_vertex_id: u64,
1235}
1236
1237/// Resolve ONE endpoint of an open rim arc against the edge that follows it
1238/// around the loop.
1239///
1240/// Two cases, and nothing else is admitted:
1241/// * the adjacent edge is the pushed face's own SEAM — the endpoint rides the
1242///   offset carrier's seam meridian, i.e. it is where this rim's conic crosses
1243///   that meridian's axial half-plane ([`seam_axial_plane`], the same closed
1244///   form the single-neighbour-hole lane pins its corners with);
1245/// * the adjacent edge's fixed neighbour `N'` is a PLANE — the endpoint is the
1246///   triple point `S' ∩ N ∩ N'`, i.e. where this rim's conic (already the
1247///   `S' ∩ N` curve) crosses `N'`. The crossing nearest the old vertex is the
1248///   branch, so a conic that meets `N'` twice picks the right corner.
1249///
1250/// A conic that LIES IN `N'` means the boolean merely split one rim in two
1251/// (`N == N'`); there is no triple point, and the split simply keeps its
1252/// azimuth on the rebuilt conic. Any other adjacent neighbour (a curved
1253/// carrier) makes `plane_of_surface` refuse, which is the fail-safe.
1254fn resolve_open_rim_end(
1255    solid: &BrepSolid,
1256    face_id: u64,
1257    faces_of_edge: &HashMap<u64, Vec<u64>>,
1258    conic: &NurbsCurve,
1259    adjacent: &EdgeRecord,
1260    old_point: Vec3,
1261    plane_tolerance: f64,
1262) -> Result<(RimEnd, Vec3), KernelRefusal> {
1263    let incident = faces_of_edge.get(&adjacent.id).cloned().unwrap_or_default();
1264    if incident.iter().all(|f| *f == face_id) {
1265        let (pshell, pface) = find_face(solid, face_id).ok_or_else(|| {
1266            KernelRefusal::internal(
1267                KernelStage::Refine,
1268                "pushed_face_seam",
1269                format!("offset_ruled_face: missing pushed face {face_id}"),
1270            )
1271        })?;
1272        let meridian = seam_axial_plane(&solid.shells[pshell].faces[pface].surface, adjacent)
1273            .ok_or_else(|| {
1274                KernelRefusal::internal(
1275                    KernelStage::Refine,
1276                    "seam_meridian",
1277                    format!(
1278                    "offset_ruled_face: the pushed face's seam (edge {}) is not a meridian of \
1279                     its carrier — refusing",
1280                    adjacent.id
1281                ),
1282                )
1283            })?;
1284        // The meridian's plane is a WHOLE axial plane, so it also carries the
1285        // opposite azimuth; the seam vertex being replaced picks the right one.
1286        // The two are a diameter apart and the new one is |r − r'| from the old,
1287        // which is smaller than r + r' for any push the carrier survives.
1288        let parameter = conic_crossing_nearest(conic, &meridian, old_point, plane_tolerance)?
1289            .ok_or_else(|| {
1290                KernelRefusal::internal(
1291                    KernelStage::Refine,
1292                    "seam_crossing",
1293                    format!(
1294                    "offset_ruled_face: the rebuilt rim never crosses the pushed face's own seam \
1295                     meridian (edge {}) — refusing",
1296                    adjacent.id
1297                ),
1298                )
1299            })?;
1300        return Ok((RimEnd::Seam(parameter), conic.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?));
1301    }
1302    let other = *incident
1303        .iter()
1304        .find(|f| **f != face_id)
1305        .ok_or_else(|| {
1306            KernelRefusal::internal(
1307                KernelStage::Refine,
1308                "adjacent_no_neighbour",
1309                format!("offset_ruled_face: edge {} has no neighbour", adjacent.id),
1310            )
1311        })?;
1312    let (nshell, nface) = find_face(solid, other).ok_or_else(|| {
1313        KernelRefusal::internal(
1314            KernelStage::Refine,
1315            "adjacent_neighbour",
1316            format!("offset_ruled_face: missing neighbour {other}"),
1317        )
1318    })?;
1319    // The fail-safe named above: a curved adjacent neighbour is a deferral, and
1320    // `plane_of_surface`'s own text says which carrier it was.
1321    let plane = plane_of_surface(
1322        &solid.shells[nshell].faces[nface].surface,
1323        plane_tolerance,
1324        "offset_ruled_face",
1325    )
1326    .map_err(|message| KernelRefusal::unsupported(KernelStage::Refine, "curved_adjacent", message))?;
1327    if curve_lies_in_plane(conic, &plane, plane_tolerance)? {
1328        let projection = project_point_to_curve(conic, old_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
1329        return Ok((RimEnd::Corner(projection.u), conic.evaluate(projection.u).or_refuse(KernelStage::Refine, "evaluate")?));
1330    }
1331    census_push("corner_crossings", || {
1332        let [d0, d1] = conic.domain().unwrap_or([f64::NAN, f64::NAN]);
1333        let height = |t: f64| conic.evaluate(t).map(|p| p.sub(plane.origin).dot(plane.normal)).unwrap_or(f64::NAN);
1334        serde_json::json!({
1335            "h_start": height(d0),
1336            "h_end": height(d1),
1337            "roots": plane_crossing_params(conic, &plane).map(|roots| roots.iter().map(|t| (t - d0) / (d1 - d0)).collect::<Vec<_>>()).unwrap_or_default(),
1338            "closed": conic.evaluate(d0).and_then(|a| conic.evaluate(d1).map(|b| a.sub(b).length())).unwrap_or(f64::NAN) <= plane_tolerance,
1339        })
1340    });
1341    let mut best: Option<(f64, f64)> = None;
1342    for parameter in plane_crossing_params(conic, &plane)? {
1343        let distance = conic.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?.sub(old_point).length();
1344        if best.map(|(best, _)| distance < best).unwrap_or(true) {
1345            best = Some((distance, parameter));
1346        }
1347    }
1348    let (_, parameter) = best.ok_or_else(|| {
1349        KernelRefusal::unsupported(
1350            KernelStage::Refine,
1351            "window_off_neighbour",
1352            "offset_ruled_face: a multi-loop rim corner no longer meets its adjacent neighbour \
1353         (the push pulled the window off it) — refusing",
1354        )
1355    })?;
1356    Ok((RimEnd::Corner(parameter), conic.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?))
1357}
1358
1359/// TRUE when the whole curve lies in the plane (so it cannot cross it): the
1360/// adjacent rim shares this rim's carrier plane and the shared vertex is a
1361/// SPLIT point, not a triple point.
1362fn curve_lies_in_plane(
1363    curve: &NurbsCurve,
1364    plane: &Plane,
1365    plane_tolerance: f64,
1366) -> Result<bool, KernelRefusal> {
1367    let [d0, d1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1368    for step in 0..=16 {
1369        let t = d0 + (d1 - d0) * step as f64 / 16.0;
1370        if curve
1371            .evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?
1372            .sub(plane.origin)
1373            .dot(plane.normal)
1374            .abs()
1375            > plane_tolerance
1376        {
1377            return Ok(false);
1378        }
1379    }
1380    Ok(true)
1381}
1382
1383/// Every parameter at which `curve` crosses `plane`, by dense sampling of the
1384/// signed plane distance plus bisection on each sign change. The curves here
1385/// are conics (a circle crosses a slot wall twice, a generatrix line crosses a
1386/// slot floor once), so a sampled sign-change sweep finds every root; bisection
1387/// then drives it to the last bit rather than to a fit tolerance.
1388fn plane_crossing_params(curve: &NurbsCurve, plane: &Plane) -> Result<Vec<f64>, KernelRefusal> {
1389    let [d0, d1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1390    let signed = |t: f64| -> Result<f64, KernelRefusal> {
1391        Ok(curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?.sub(plane.origin).dot(plane.normal))
1392    };
1393    const SAMPLES: usize = 512;
1394    let mut roots = Vec::new();
1395    let mut previous = (d0, signed(d0)?);
1396    for index in 1..=SAMPLES {
1397        let t = d0 + (d1 - d0) * index as f64 / SAMPLES as f64;
1398        let value = signed(t)?;
1399        if previous.1 == 0.0 {
1400            roots.push(previous.0);
1401        } else if (previous.1 < 0.0) != (value < 0.0) {
1402            let (mut low, mut high) = (previous.0, t);
1403            let mut low_value = previous.1;
1404            for _ in 0..100 {
1405                let middle = 0.5 * (low + high);
1406                if middle <= low || middle >= high {
1407                    break;
1408                }
1409                let middle_value = signed(middle)?;
1410                if (low_value < 0.0) != (middle_value < 0.0) {
1411                    high = middle;
1412                } else {
1413                    low = middle;
1414                    low_value = middle_value;
1415                }
1416            }
1417            roots.push(0.5 * (low + high));
1418        }
1419        previous = (t, value);
1420    }
1421    if previous.1 == 0.0 {
1422        roots.push(previous.0);
1423    }
1424    Ok(roots)
1425}
1426
1427/// The parameter at which `curve` crosses `plane` NEAREST `reference`, or
1428/// `None` when it never crosses it.
1429///
1430/// Written for the seam endpoint of an open rim, where the plane is an axial
1431/// half-plane's whole plane and the crossing wanted is the one replacing a
1432/// known old point. Two things [`plane_crossing_params`] alone does not give:
1433///
1434/// * **the domain ENDS count.** A closed conic whose parameter origin already
1435///   sits on the plane — the canonical case, where the carrier's seam azimuth
1436///   IS `frame.x_axis` — has a root exactly at `d0` (== `d1`). The sampled
1437///   sweep only reports it when the signed distance there is *bit*-zero, and
1438///   a bisected near-endpoint root is a few ulp off the exact end. So both
1439///   ends are offered first, and a root that duplicates an offered end is
1440///   dropped: the canonical case then returns `d0` exactly, and
1441///   `conic.evaluate(d0)` stays the point this function has always returned.
1442/// * **a pick.** An axial plane cuts a conic about the same axis twice, at
1443///   opposite azimuths; only the old point can say which is the seam's.
1444fn conic_crossing_nearest(
1445    curve: &NurbsCurve,
1446    plane: &Plane,
1447    reference: Vec3,
1448    plane_tolerance: f64,
1449) -> Result<Option<f64>, KernelRefusal> {
1450    let [d0, d1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1451    let span = (d1 - d0).max(1e-12);
1452    let mut candidates: Vec<f64> = Vec::new();
1453    let mut ends = [false, false];
1454    for (slot, end) in [d0, d1].into_iter().enumerate() {
1455        if curve.evaluate(end).or_refuse(KernelStage::Refine, "evaluate")?.sub(plane.origin).dot(plane.normal).abs() <= plane_tolerance {
1456            ends[slot] = true;
1457            candidates.push(end);
1458        }
1459    }
1460    for root in plane_crossing_params(curve, plane)? {
1461        if (ends[0] && (root - d0).abs() <= 1e-6 * span)
1462            || (ends[1] && (root - d1).abs() <= 1e-6 * span)
1463        {
1464            continue;
1465        }
1466        candidates.push(root);
1467    }
1468    let mut best: Option<(f64, f64)> = None;
1469    for parameter in candidates {
1470        let distance = curve.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?.sub(reference).length();
1471        if best.map(|(known, _)| distance < known).unwrap_or(true) {
1472            best = Some((distance, parameter));
1473        }
1474    }
1475    Ok(best.map(|(_, parameter)| parameter))
1476}
1477
1478/// The piece of `curve` over `[from, to]`. `NurbsCurve::split` keeps the parent
1479/// parameterization, so the result's own domain IS `[from, to]` — which is what
1480/// the edge's `t0`/`t1` are then set from.
1481fn subcurve(curve: &NurbsCurve, from: f64, to: f64) -> Result<NurbsCurve, KernelRefusal> {
1482    let [d0, d1] = curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
1483    let span = (d1 - d0).max(1e-12);
1484    if to - from <= 1e-9 * span {
1485        return Err(KernelRefusal::unsupported(
1486            KernelStage::Fragment,
1487            "arc_collapse",
1488            "offset_ruled_face: a rebuilt rim arc collapsed to a point — refusing",
1489        ));
1490    }
1491    let mut trimmed = curve.clone();
1492    if to < d1 - 1e-9 * span {
1493        trimmed = trimmed.split(to).or_refuse(KernelStage::Fragment, "split")?.0;
1494    }
1495    if from > d0 + 1e-9 * span {
1496        trimmed = trimmed.split(from).or_refuse(KernelStage::Fragment, "split")?.1;
1497    }
1498    Ok(trimmed)
1499}
1500
1501/// The arc of `conic` between the parameters `low <= high` that CONTAINS
1502/// `middle` — the old edge's own midpoint, which is what says which of the two
1503/// complementary arcs the edge being replaced actually was.
1504///
1505/// `[low, high]` when the midpoint lies in it. Otherwise the edge is the
1506/// COMPLEMENT, the arc that runs the other way round across the conic's period
1507/// boundary, and that is an ordinary sub-arc exactly when one of its two halves
1508/// — `[d0, low]` and `[high, d1]` — is empty, i.e. when an endpoint landed ON
1509/// the boundary. It does, whenever a rim was split at the pushed carrier's own
1510/// seam azimuth: the reporter's gear bore (`IMPORT3D1_Face_29`) carries such a
1511/// split, and its two arcs there are `[0.9946, 1]` (endpoint at 0) and
1512/// `[0, 0.5]` (endpoint at 1). Both used to be refused as "wraps the pushed
1513/// carrier's periodic seam" although neither needs anything joined.
1514///
1515/// The genuinely two-piece complement stays a refusal: joining two rational
1516/// NURBS pieces across the boundary is a curve rebuild this lane does not do,
1517/// and no corpus document asks for one.
1518fn rim_arc_between(
1519    conic: &NurbsCurve,
1520    low: f64,
1521    high: f64,
1522    middle: f64,
1523    tolerance: f64,
1524) -> Result<NurbsCurve, KernelRefusal> {
1525    let [d0, d1] = conic.domain().or_refuse(KernelStage::Fragment, "domain")?;
1526    if middle >= low && middle <= high {
1527        return subcurve(conic, low, high);
1528    }
1529    // The two halves of the complement can only be ONE arc if the conic closes.
1530    if conic.evaluate(d0).or_refuse(KernelStage::Fragment, "evaluate")?.sub(conic.evaluate(d1).or_refuse(KernelStage::Fragment, "evaluate")?).length() > tolerance {
1531        return Err(KernelRefusal::internal(
1532            KernelStage::Fragment,
1533            "open_conic",
1534            "offset_ruled_face: a multi-loop rim arc runs outside its OPEN conic's domain — \
1535             refusing",
1536        ));
1537    }
1538    let span = (d1 - d0).max(1e-12);
1539    let head_is_empty = low - d0 <= 1e-9 * span;
1540    let tail_is_empty = d1 - high <= 1e-9 * span;
1541    match (head_is_empty, tail_is_empty) {
1542        (true, false) => subcurve(conic, high, d1),
1543        (false, true) => subcurve(conic, d0, low),
1544        _ => Err(KernelRefusal::unsupported(
1545            KernelStage::Fragment,
1546            "two_piece_arc",
1547            "offset_ruled_face: a multi-loop rim arc crosses the rebuilt conic's period \
1548             boundary with material on BOTH sides, so it is two pieces to join — deferred \
1549             (refusing)",
1550        )),
1551    }
1552}
1553
1554/// Orient a rebuilt CLOSED rim the way the edge it replaces ran.
1555///
1556/// `intersect_analytic_pair` always emits its conics in its OWN direction
1557/// (increasing azimuth about the carrier frame). The edge being replaced need
1558/// not run that way: a wall's top and bottom cap circles are traversed in
1559/// OPPOSITE directions around the face loop, so one of them arrives decreasing.
1560/// Both are the same point set and a closed rim's two endpoints are the same
1561/// vertex, so `validate` cannot tell them apart — but the face's pcurve for the
1562/// reversed one runs `u: 1 → 0`, and handing the loop an increasing rim tears it
1563/// open in parameter space, which the Green's-theorem volume integral reads as a
1564/// completely different solid (measured 138.18 against a true 1024.11).
1565///
1566/// Decided on the START TANGENT, which is local and exact: a closed rim starts
1567/// on the carrier's seam and so does the rebuilt conic, so the two tangents
1568/// there either agree or oppose. OPEN rims are not orientated here — they are
1569/// trimmed to their solved corners first and then turned to run
1570/// start-vertex → end-vertex.
1571fn match_closed_rim_direction(
1572    rim: NurbsCurve,
1573    previous: &EdgeRecord,
1574) -> Result<NurbsCurve, KernelRefusal> {
1575    let [d0, _] = rim.domain().or_refuse(KernelStage::Refine, "domain")?;
1576    let incoming = previous.curve.derivatives(previous.t0, 1).or_refuse(KernelStage::Refine, "derivatives")?;
1577    let rebuilt = rim.derivatives(d0, 1).or_refuse(KernelStage::Refine, "derivatives")?;
1578    if incoming[1].dot(rebuilt[1]) < 0.0 {
1579        return rim.reversed().or_refuse(KernelStage::Refine, "reversed");
1580    }
1581    Ok(rim)
1582}
1583
1584/// Put a freshly fitted pcurve back on the periodic BRANCH of `u` that the
1585/// pcurve it replaces used, by the whole-period shift that aligns their starts.
1586///
1587/// This is not cosmetic. `build_pcurve_on_surface` CLAMPS an analytic carrier's
1588/// parameters into `[u0, u1]`, but a face loop on a full revolution legitimately
1589/// carries `u` one period PAST the domain: the two coedges of the periodic seam
1590/// sit on opposite sides of it, so the coedge that closes the loop across the
1591/// seam runs (say) `u: 1 → 2`. `validate` accepts either branch — it evaluates
1592/// the surface with `evaluate_extended`, which wraps — but the Green's-theorem
1593/// area/volume integral does NOT: it integrates the wire as drawn in parameter
1594/// space, and a rim dropped back to `u: 0 → 1` tears the loop open and yields a
1595/// nonsense volume (measured 138.18 for a solid whose true volume is 1024.11).
1596///
1597/// The replacement rim traverses the same path in the same direction as the edge
1598/// it replaces — only its parameter DISTRIBUTION and (for an open arc) its
1599/// endpoints change — so aligning the starts fixes the branch, and the endpoint
1600/// check refuses anything that is not merely re-anchored.
1601fn reanchor_pcurve_u(
1602    pcurve: &NurbsCurve,
1603    previous: &NurbsCurve,
1604    u_period: f64,
1605) -> Result<NurbsCurve, KernelRefusal> {
1606    if !(u_period.is_finite() && u_period > 0.0) {
1607        return Ok(pcurve.clone());
1608    }
1609    let [a0, a1] = pcurve.domain().or_refuse(KernelStage::Sew, "domain")?;
1610    let [b0, b1] = previous.domain().or_refuse(KernelStage::Sew, "domain")?;
1611    let shift = ((previous.evaluate(b0).or_refuse(KernelStage::Sew, "evaluate")?.x - pcurve.evaluate(a0).or_refuse(KernelStage::Sew, "evaluate")?.x) / u_period).round() * u_period;
1612    let shifted = if shift == 0.0 {
1613        pcurve.clone()
1614    } else {
1615        let controls = pcurve
1616            .control_points
1617            .iter()
1618            .map(|control| {
1619                let mut point = control.point().or_refuse(KernelStage::Sew, "point")?;
1620                point.x += shift;
1621                Ok(crate::Vec4::from_point(point, control.w))
1622            })
1623            .collect::<Result<Vec<_>, KernelRefusal>>()?;
1624        NurbsCurve::new(pcurve.degree, pcurve.knots.clone(), controls).or_refuse(KernelStage::Sew, "nurbs_curve")?
1625    };
1626    // Both ends must land within a quarter period of the ones they replace: a
1627    // rim that reversed direction, or that jumped a branch mid-curve, is not a
1628    // re-anchoring and must not be silently accepted.
1629    let end_drift = (shifted.evaluate(a1).or_refuse(KernelStage::Sew, "evaluate")?.x - previous.evaluate(b1).or_refuse(KernelStage::Sew, "evaluate")?.x).abs();
1630    if end_drift > 0.25 * u_period {
1631        return Err(KernelRefusal::internal(
1632            KernelStage::Sew,
1633            "pcurve_reanchor",
1634            format!(
1635            "offset_ruled_face: a rebuilt rim traverses the carrier's periodic parameter \
1636             differently from the edge it replaces (end drift {end_drift}) — refusing"
1637        ),
1638        ));
1639    }
1640    Ok(shifted)
1641}
1642
1643/// TRUE when `neighbour` is a ruled revolution (cylinder or cone) sharing the
1644/// pushed carrier's axis LINE — same axis direction (up to sign) and the axes
1645/// coincident. Only such a neighbour has a closed-form re-intersection with the
1646/// offset carrier (`intersect_coaxial_revolutions`); the tolerances mirror that
1647/// intersector so an accepted neighbour is one it can actually solve.
1648pub(super) fn ruled_neighbour_is_coaxial(
1649    neighbour: &NurbsSurface,
1650    axis_origin: Vec3,
1651    axis_dir: Vec3,
1652    scale: f64,
1653) -> bool {
1654    let Some(AnalyticSurface::RuledRevolution { frame, .. }) = neighbour.analytic() else {
1655        return false;
1656    };
1657    if frame.axis.dot(axis_dir).abs() < 1.0 - 1e-9 {
1658        return false;
1659    }
1660    let offset = frame.origin.sub(axis_origin);
1661    let perpendicular = offset.sub(axis_dir.scale(offset.dot(axis_dir)));
1662    perpendicular.length() <= 1e-9 * scale.max(1.0)
1663}
1664
1665/// Re-trim a ruled-revolution face (the pushed carrier itself, now S′, or a
1666/// coaxial ruled neighbour) whose boundary moved axially: grow the carrier along
1667/// its axis to cover the updated boundary (`extend_ruled_neighbour_over`, exact),
1668/// then rebuild every pcurve from the already-updated edge curves. The offset
1669/// analogue of `retrim_planar_face` — all loops are visited, so holes carry.
1670///
1671/// The three-phase body is `crate::offset_retrim::retrim_face_in_solid`; this is
1672/// the ruled growth strategy and the `offset_ruled_face` refusal prefix.
1673pub(super) fn retrim_offset_ruled_face(
1674    result: &mut BrepSolid,
1675    face_id: u64,
1676    final_edges: &HashMap<u64, EdgeRecord>,
1677    tolerance: f64,
1678) -> Result<(), KernelRefusal> {
1679    let (shell, face_pos) = find_face(result, face_id).ok_or_else(|| {
1680        KernelRefusal::internal(
1681            KernelStage::Sew,
1682            "missing_ruled_face",
1683            format!("offset_ruled_face: missing ruled face {face_id}"),
1684        )
1685    })?;
1686    retrim_face_in_solid(
1687        result,
1688        shell,
1689        face_pos,
1690        final_edges,
1691        |solid, points| Ok(extend_ruled_neighbour_over(solid, face_id, points, tolerance)?),
1692        tolerance,
1693        "offset_ruled_face",
1694    )
1695    
1696}
1697
1698/// A window through the wall bounded entirely by ONE crossing carrier.
1699struct SingleNeighbourHole {
1700    neighbour: u64,
1701    /// Every edge of the loop, in loop order, de-duplicated.
1702    edge_ids: Vec<u64>,
1703    /// Corner vertices that are ALSO the end of one of the neighbour's own
1704    /// edges — its seam meridian, in every case the corpus reaches. Such a
1705    /// corner is NOT a free bookkeeping split: it is pinned to that meridian,
1706    /// so it moves along it rather than to the nearest point of the new
1707    /// section. `(vertex id, the neighbour edge it is pinned to)`.
1708    pinned: Vec<(u64, u64)>,
1709}
1710
1711/// The axial half-plane a revolution carrier's SEAM meridian lies in.
1712///
1713/// Every seam of every revolution carrier this lane admits — a cylinder, a
1714/// cone, a sphere, a torus, a general revolution — is a meridian, and a
1715/// meridian lies in the half-plane spanned by the axis and its own radial
1716/// direction. So "where does the new section cross the neighbour's seam" is a
1717/// curve × plane crossing, in closed form, for all five at once. Returns `None`
1718/// for a carrier that is not a revolution or a seam that runs ON the axis.
1719fn seam_axial_plane(surface: &NurbsSurface, seam: &EdgeRecord) -> Option<Plane> {
1720    let structure = crate::revolution_structure(surface)?;
1721    let midpoint = seam
1722        .curve
1723        .evaluate(0.5 * (seam.t0 + seam.t1))
1724        .ok()?;
1725    let offset = midpoint.sub(structure.frame.origin);
1726    let radial = offset
1727        .sub(structure.frame.axis.scale(offset.dot(structure.frame.axis)))
1728        .normalized()
1729        .ok()?;
1730    let normal = structure.frame.axis.cross(radial).normalized().ok()?;
1731    Some(Plane {
1732        origin: structure.frame.origin,
1733        u_dir: structure.frame.axis,
1734        v_dir: radial,
1735        normal,
1736    })
1737}
1738
1739/// Recognise a loop that is a hole cut by ONE crossing carrier, or say why not.
1740///
1741/// Four conditions, each of which the rebuild depends on and none of which it
1742/// can check afterwards:
1743///
1744/// * every coedge's other face is the SAME neighbour — otherwise a corner IS a
1745///   triple point and belongs to `resolve_open_rim_end`;
1746/// * that neighbour is neither planar nor coaxial-ruled — the two lanes with
1747///   their own exact rebuilds, which must keep them (bit-identity);
1748/// * every edge is OPEN (a closed edge is the single-rim lane's business) —
1749///   except a loop that is ONE closed edge whose vertex is PINNED to the
1750///   neighbour's seam. That vertex is a corner, not the bookkeeping split the
1751///   single-rim lane places at the march's domain start (which lands it off the
1752///   seam: 2.923 on the crossing-pipe tee), and it is exactly the two-arc
1753///   window this lane already rebuilt, stored as the one edge it is;
1754/// * every corner vertex has valence two across the WHOLE solid, so relocating
1755///   it cannot strand a third face's boundary. This is what rules out the hole
1756///   that straddles the pushed carrier's periodic seam: its arcs are stitched
1757///   into the outer loop, so the loop fails the first condition long before the
1758///   valence test — and either way it is refused rather than torn.
1759///
1760/// Returns `Ok(None)` for a loop that is simply not this shape (the outer loop
1761/// of any ordinary push), so the caller falls through to the lanes it always
1762/// used.
1763#[allow(clippy::too_many_arguments)]
1764fn single_neighbour_hole(
1765    loop_record: &crate::topology::LoopRecord,
1766    face_id: u64,
1767    solid: &BrepSolid,
1768    faces_of_edge: &HashMap<u64, Vec<u64>>,
1769    edge_by_id: &HashMap<u64, &EdgeRecord>,
1770    frame_origin: Vec3,
1771    frame_axis: Vec3,
1772    scale: f64,
1773) -> Result<Option<SingleNeighbourHole>, KernelRefusal> {
1774    let debug = std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok();
1775    let mut neighbour: Option<u64> = None;
1776    let mut edge_ids: Vec<u64> = Vec::new();
1777    for coedge in &loop_record.coedges {
1778        let edge = *edge_by_id.get(&coedge.edge_id).ok_or_else(|| {
1779            KernelRefusal::internal(
1780                KernelStage::Classify,
1781                "hole_edge",
1782                format!("offset_ruled_face: missing edge {}", coedge.edge_id),
1783            )
1784        })?;
1785        if edge.start_vertex_id == edge.end_vertex_id && loop_record.coedges.len() != 1 {
1786            if debug { eprintln!("HOLE reject: edge {} is closed", edge.id); }
1787            return Ok(None); // a closed rim among others: not one window.
1788        }
1789        let incident = faces_of_edge.get(&edge.id).cloned().unwrap_or_default();
1790        let others: Vec<u64> = incident.into_iter().filter(|f| *f != face_id).collect();
1791        if others.len() != 1 {
1792            if debug { eprintln!("HOLE reject: edge {} has {} others", edge.id, others.len()); }
1793            return Ok(None); // a seam, or a non-manifold edge.
1794        }
1795        match neighbour {
1796            Some(known) if known != others[0] => {
1797                if debug { eprintln!("HOLE reject: mixed neighbours {known} / {}", others[0]); }
1798                return Ok(None);
1799            }
1800            Some(_) => {}
1801            None => neighbour = Some(others[0]),
1802        }
1803        if !edge_ids.contains(&edge.id) {
1804            edge_ids.push(edge.id);
1805        }
1806    }
1807    let Some(neighbour) = neighbour else {
1808        return Ok(None);
1809    };
1810    // A lone CLOSED edge qualifies only once its vertex is shown to be pinned
1811    // (below); an OPEN lone edge never closes a window.
1812    let lone_closed = edge_ids.len() == 1
1813        && edge_by_id
1814            .get(&edge_ids[0])
1815            .is_some_and(|edge| edge.start_vertex_id == edge.end_vertex_id);
1816    if edge_ids.len() < 2 && !lone_closed {
1817        if debug { eprintln!("HOLE reject: only {} edges", edge_ids.len()); }
1818        return Ok(None);
1819    }
1820    let (nshell, nface) = find_face(solid, neighbour).ok_or_else(|| {
1821        KernelRefusal::internal(
1822            KernelStage::Classify,
1823            "hole_neighbour",
1824            format!("offset_ruled_face: missing neighbour {neighbour}"),
1825        )
1826    })?;
1827    let surface = &solid.shells[nshell].faces[nface].surface;
1828    if matches!(surface.analytic(), Some(AnalyticSurface::Plane { .. }))
1829        || ruled_neighbour_is_coaxial(surface, frame_origin, frame_axis, scale)
1830    {
1831        if debug { eprintln!("HOLE reject: neighbour {neighbour} is planar/coaxial"); }
1832        return Ok(None); // the two lanes that already have exact rebuilds.
1833    }
1834    // Every corner is either a free bookkeeping split (nothing else ends there)
1835    // or PINNED to one edge of the neighbour — its seam. Anything else ending
1836    // at a corner makes it a genuine junction of three or more faces, which
1837    // this lane does not solve.
1838    let neighbour_edges: HashSet<u64> = solid.shells[nshell].faces[nface]
1839        .loops
1840        .iter()
1841        .flat_map(|loop_record| &loop_record.coedges)
1842        .map(|coedge| coedge.edge_id)
1843        .collect();
1844    let mut pinned: Vec<(u64, u64)> = Vec::new();
1845    for edge_id in &edge_ids {
1846        let edge = *edge_by_id.get(edge_id).ok_or_else(|| {
1847            KernelRefusal::internal(
1848                KernelStage::Classify,
1849                "hole_pin_edge",
1850                format!("offset_ruled_face: missing edge {edge_id}"),
1851            )
1852        })?;
1853        for vertex_id in [edge.start_vertex_id, edge.end_vertex_id] {
1854            for other in &solid.edges {
1855                if other.start_vertex_id != vertex_id && other.end_vertex_id != vertex_id {
1856                    continue;
1857                }
1858                if edge_ids.contains(&other.id) {
1859                    continue;
1860                }
1861                if !neighbour_edges.contains(&other.id) {
1862                    if debug {
1863                        eprintln!("HOLE reject: vertex {vertex_id} also on foreign edge {}", other.id);
1864                    }
1865                    return Ok(None);
1866                }
1867                if pinned
1868                    .iter()
1869                    .any(|(known, pin)| *known == vertex_id && *pin != other.id)
1870                {
1871                    if debug {
1872                        eprintln!("HOLE reject: vertex {vertex_id} pinned by two neighbour edges");
1873                    }
1874                    return Ok(None);
1875                }
1876                if !pinned.iter().any(|(known, _)| *known == vertex_id) {
1877                    pinned.push((vertex_id, other.id));
1878                }
1879            }
1880        }
1881    }
1882    if lone_closed && pinned.is_empty() {
1883        // A free closed rim: its vertex is bookkeeping, the single-rim lane's.
1884        if debug { eprintln!("HOLE reject: lone closed edge {} is not pinned", edge_ids[0]); }
1885        return Ok(None);
1886    }
1887    if debug {
1888        eprintln!("HOLE accept: neighbour {neighbour} edges {edge_ids:?} pinned {pinned:?}");
1889    }
1890    Ok(Some(SingleNeighbourHole {
1891        neighbour,
1892        edge_ids,
1893        pinned,
1894    }))
1895}
1896
1897/// Rebuild a single-neighbour hole: re-intersect once, then cut the section.
1898#[allow(clippy::too_many_arguments)]
1899fn rebuild_single_neighbour_hole(
1900    solid: &BrepSolid,
1901    s_prime: &NurbsSurface,
1902    hole: &SingleNeighbourHole,
1903    edge_by_id: &HashMap<u64, &EdgeRecord>,
1904    vertex_pos: &HashMap<u64, Vec3>,
1905    tolerance: f64,
1906    scale: f64,
1907    new_curve: &mut HashMap<u64, NurbsCurve>,
1908    new_vertex: &mut HashMap<u64, Vec3>,
1909) -> Result<(), KernelRefusal> {
1910    let (nshell, nface) = find_face(solid, hole.neighbour).ok_or_else(|| {
1911        KernelRefusal::internal(
1912            KernelStage::Refine,
1913            "hole_rebuild_neighbour",
1914            format!("offset_ruled_face: missing neighbour {}", hole.neighbour),
1915        )
1916    })?;
1917    let neighbour_surface = &solid.shells[nshell].faces[nface].surface;
1918
1919    // Seed the march with the WHOLE old loop: the section replacing it runs
1920    // near it for any push small against the feature, and a blind seed grid on
1921    // two large carriers can miss a small window entirely.
1922    let mut seeds: Vec<Vec3> = Vec::new();
1923    let mut reference: Vec<Vec3> = Vec::new();
1924    for edge_id in &hole.edge_ids {
1925        let edge = *edge_by_id.get(edge_id).ok_or_else(|| {
1926            KernelRefusal::internal(
1927                KernelStage::Refine,
1928                "hole_seed_edge",
1929                format!("offset_ruled_face: missing edge {edge_id}"),
1930            )
1931        })?;
1932        let samples = edge_seeds(edge, 9)?;
1933        reference.extend(samples.iter().copied());
1934        seeds.extend(samples);
1935    }
1936    let policy = MarchPolicy {
1937        tolerance,
1938        residual_tolerance: (scale * 5e-4).max(5e-6),
1939        seeds,
1940    };
1941    let found = match reintersect_carriers(s_prime, neighbour_surface, &policy) {
1942        Ok(found) => found,
1943        Err(ReintersectRefusal::Separated) => {
1944            return Err(KernelRefusal::unsupported(
1945                KernelStage::Refine,
1946                "hole_separated",
1947                "offset_ruled_face: the pushed carrier no longer meets a neighbour \
1948                 (the push separated them, or the rim left the neighbour's domain) — refusing",
1949            ))
1950        }
1951        Err(other @ ReintersectRefusal::Residual { .. }) => {
1952            return Err(KernelRefusal::non_convergence(
1953                KernelStage::Refine,
1954                "hole_reintersect_residual",
1955                format!("offset_ruled_face: {}", other.describe()),
1956            ))
1957        }
1958        Err(other) => {
1959            return Err(KernelRefusal::internal(
1960                KernelStage::Refine,
1961                "hole_reintersect_failed",
1962                format!("offset_ruled_face: {}", other.describe()),
1963            ))
1964        }
1965    };
1966    if found.lane != RimLane::Marched {
1967        // An analytic section has no polyline to cut, and the exact lanes that
1968        // produce one already have their own arc rebuild. Refusing here keeps
1969        // this lane from re-deciding a case the closed forms own.
1970        return Err(KernelRefusal::unsupported(
1971            KernelStage::Refine,
1972            "hole_closed_form",
1973            format!(
1974            "offset_ruled_face: the window bounded by face {} re-intersects in CLOSED FORM, \
1975             whose arc rebuild is the analytic lane's — deferred (refusing)",
1976            hole.neighbour
1977        ),
1978        ));
1979    }
1980    if std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok() {
1981        eprintln!(
1982            "HOLE rebuild neighbour {}: lane {:?}, {} branch(es), residual {:.3e} \
1983             (gate {:.3e})",
1984            hole.neighbour,
1985            found.lane,
1986            found.sections.len(),
1987            found.residual,
1988            policy.residual_tolerance
1989        );
1990    }
1991    // Which branch is THIS window: two windows cut by one crossing carrier
1992    // share a surface and so come back as two branches of one section set.
1993    let section = found
1994        .nearest_section(&reference)
1995        .map_err(|error| {
1996            KernelRefusal::internal(
1997                KernelStage::Refine,
1998                "nearest_section",
1999                format!("offset_ruled_face: {error}"),
2000            )
2001        })?;
2002
2003    // Corners first, so every arc is cut against the same relocated vertices.
2004    //
2005    // A corner PINNED to the neighbour's seam is not free to go to the nearest
2006    // sample: it must stay on that meridian, so it goes where the new section
2007    // CROSSES the meridian's axial half-plane. Placing it at the nearest sample
2008    // instead leaves it off the seam by the amount the section drifted, which
2009    // the neighbour's own re-trim then reports as "a relocated rim vertex left
2010    // curved neighbour N's edge" — a refusal where a correct answer exists.
2011    for edge_id in &hole.edge_ids {
2012        let edge = *edge_by_id.get(edge_id).ok_or_else(|| {
2013            KernelRefusal::internal(
2014                KernelStage::Fragment,
2015                "hole_corner_edge",
2016                format!("offset_ruled_face: missing edge {edge_id}"),
2017            )
2018        })?;
2019        for vertex_id in [edge.start_vertex_id, edge.end_vertex_id] {
2020            if new_vertex.contains_key(&vertex_id) {
2021                continue;
2022            }
2023            let old = *vertex_pos.get(&vertex_id).ok_or_else(|| {
2024                KernelRefusal::internal(
2025                    KernelStage::Fragment,
2026                    "hole_corner_vertex",
2027                    format!("offset_ruled_face: missing vertex {vertex_id}"),
2028                )
2029            })?;
2030            let point = match hole
2031                .pinned
2032                .iter()
2033                .find(|(known, _)| *known == vertex_id)
2034                .map(|(_, pin)| *pin)
2035            {
2036                Some(pin) => {
2037                    let seam = *edge_by_id.get(&pin).ok_or_else(|| {
2038                        KernelRefusal::internal(
2039                            KernelStage::Fragment,
2040                            "hole_pin_seam",
2041                            format!("offset_ruled_face: missing edge {pin}"),
2042                        )
2043                    })?;
2044                    let plane = seam_axial_plane(neighbour_surface, seam).ok_or_else(|| {
2045                        KernelRefusal::unsupported(
2046                            KernelStage::Fragment,
2047                            "pinned_non_revolution",
2048                            format!(
2049                            "offset_ruled_face: the window's corner is pinned to edge {pin} of a \
2050                             neighbour that is not a surface of revolution — refusing"
2051                        ),
2052                        )
2053                    })?;
2054                    // The correct crossing is inside the window itself, so the
2055                    // search reach is the window's own extent about this corner
2056                    // — the opposite meridian cannot win.
2057                    let reach = reference
2058                        .iter()
2059                        .map(|point| point.sub(old).length())
2060                        .fold(0.0f64, f64::max)
2061                        .max(tolerance * 100.0);
2062                    // A strict sign change, not `curve_plane_crossing_or_end_near`:
2063                    // `arc_of_section` cuts only a CLOSED traced section, and a
2064                    // closed curve does not stop on the seam plane — it passes
2065                    // through it, and where its parameter origin lies on the
2066                    // plane its two ends carry the same round-off, so the first
2067                    // or the last sampled interval brackets that crossing either
2068                    // way. Measured on every pinned corner in the lib, suite and
2069                    // case populations: the strict and the end-accepting crossing
2070                    // are the same point.
2071                    census_push("seam_crossings", || {
2072                        crossing_note(
2073                            &section.curve,
2074                            &plane,
2075                            curve_plane_crossing_near(&section.curve, &plane, old, reach),
2076                            curve_plane_crossing_or_end_near(&section.curve, &plane, old, reach, tolerance),
2077                            section.closed,
2078                        )
2079                    });
2080                    curve_plane_crossing_near(&section.curve, &plane, old, reach)
2081                        .map(|(_, point)| point)
2082                        .ok_or_else(|| {
2083                            KernelRefusal::internal(
2084                                KernelStage::Fragment,
2085                                "pinned_seam_crossing",
2086                                format!(
2087                                "offset_ruled_face: the rebuilt window never crosses the seam \
2088                                 (edge {pin}) its corner is pinned to — refusing"
2089                            ),
2090                            )
2091                        })?
2092                }
2093                None => section_corner(section, old).map_err(|error| {
2094                    KernelRefusal::internal(
2095                        KernelStage::Fragment,
2096                        "section_corner",
2097                        format!("offset_ruled_face: {error}"),
2098                    )
2099                })?,
2100            };
2101            new_vertex.insert(vertex_id, point);
2102        }
2103    }
2104    for edge_id in &hole.edge_ids {
2105        let edge = *edge_by_id.get(edge_id).ok_or_else(|| {
2106            KernelRefusal::internal(
2107                KernelStage::Fragment,
2108                "hole_arc_edge",
2109                format!("offset_ruled_face: missing edge {edge_id}"),
2110            )
2111        })?;
2112        let from = new_vertex[&edge.start_vertex_id];
2113        let to = new_vertex[&edge.end_vertex_id];
2114        // A closed edge asks `arc_of_section` for the whole section, whose
2115        // direction is read off a point the old edge reaches early: a quarter
2116        // of the way round by ARC LENGTH, and an open arc's midpoint the same
2117        // way. A fraction of the edge's PARAMETER range is that point only for a
2118        // uniform speed law; the crossing-pipe tee's window re-weighted by a
2119        // Möbius substitution put its quarter-parameter point past the half
2120        // turn, cut the section the wrong way round, and refused as "a rebuilt
2121        // rim traverses the carrier's periodic parameter differently".
2122        let through = if edge.start_vertex_id == edge.end_vertex_id {
2123            arc_length_stations(edge, 5, true)?[1]
2124        } else {
2125            arc_length_stations(edge, 3, true)?[1]
2126        };
2127        let arc = arc_of_section(section, from, to, through, tolerance).map_err(|error| {
2128            KernelRefusal::internal(
2129                KernelStage::Fragment,
2130                "arc_of_section",
2131                format!("offset_ruled_face: {error}"),
2132            )
2133        })?;
2134        new_curve.insert(edge.id, arc);
2135    }
2136    Ok(())
2137}
2138
2139/// Re-fit ONLY the pcurves whose edge actually changed, each back onto the
2140/// periodic branch of `u` the pcurve it replaces was on.
2141///
2142/// This is the CURVED-neighbour lane's re-trim, and it is deliberately not
2143/// [`crate::offset_retrim::retrim_face_in_solid`]. That driver rebuilds *every*
2144/// pcurve of the face, which is right for a ruled revolution (whose seam
2145/// coedges are straight generatrices whose rebuilt pcurves land back on their
2146/// own sides by luck of the parameterization) and **measurably wrong** for a
2147/// sphere or a torus. Measured on the ball-capped rod, `d = +0.5`:
2148///
2149/// | face | rebuilt-all | correct (independently built r = 3.5 solid) |
2150/// |---|---|---|
2151/// | wall seam coedge | `u: 0 → 0` | `u: 1 → 1` |
2152/// | dome seam coedge | `u: 0 → 0` | `u: 1 → 1` |
2153/// | dome pole coedge | `(0,1) → (0,1)` | `(1,1) → (0,1)` |
2154///
2155/// with a resulting solid that `validate()` accepts and whose Green's-theorem
2156/// volume reads **207.86 against the true 534.10** — the dome's parameter-space
2157/// loop collapsed to zero area because both of its seam sides ended up on the
2158/// same branch. Exactly the failure [`reanchor_pcurve_u`] was written for.
2159///
2160/// Two rules together fix it, and both are needed:
2161/// * **touch only what moved.** A neighbour that keeps its surface keeps every
2162///   pcurve whose edge did not change; rebuilding one is a chance to land on
2163///   the wrong branch for no gain.
2164/// * **re-anchor what is rebuilt**, by the whole-period shift that aligns its
2165///   start with the pcurve it replaces — the rebuilt rim traverses the same path
2166///   in the same direction, so aligning the starts fixes the branch, and
2167///   [`reanchor_pcurve_u`]'s end-drift check refuses anything that is not
2168///   merely re-anchored.
2169fn refit_changed_pcurves(
2170    result: &mut BrepSolid,
2171    face_id: u64,
2172    final_edges: &HashMap<u64, EdgeRecord>,
2173    changed: &HashSet<u64>,
2174    tolerance: f64,
2175) -> Result<(), KernelRefusal> {
2176    let (shell, face_pos) = find_face(result, face_id).ok_or_else(|| {
2177        KernelRefusal::internal(
2178            KernelStage::Sew,
2179            "refit_face",
2180            format!("offset_ruled_face: missing face {face_id}"),
2181        )
2182    })?;
2183    let surface = result.shells[shell].faces[face_pos].surface.clone();
2184    let [u_start, u_end] = surface.domain_u().or_refuse(KernelStage::Sew, "domain_u")?;
2185    let u_period = u_end - u_start;
2186    for loop_record in &mut result.shells[shell].faces[face_pos].loops {
2187        for coedge in &mut loop_record.coedges {
2188            if !changed.contains(&coedge.edge_id) {
2189                continue;
2190            }
2191            let edge = final_edges.get(&coedge.edge_id).ok_or_else(|| {
2192                KernelRefusal::internal(
2193                    KernelStage::Sew,
2194                    "refit_edge",
2195                    format!("offset_ruled_face: missing edge {}", coedge.edge_id),
2196                )
2197            })?;
2198            // Same subrange rule as `offset_retrim::rebuild_loop_pcurves`, so a rim that is
2199            // a strict piece of a full-domain curve keeps the range-aware fit it
2200            // has always had.
2201            let [d0, d1] = edge.curve.domain().or_refuse(KernelStage::Sew, "domain")?;
2202            let span = (d1 - d0).max(1e-12);
2203            let is_subrange =
2204                (edge.t0 - d0).abs() > 1e-9 * span || (edge.t1 - d1).abs() > 1e-9 * span;
2205            let pcurve = if is_subrange {
2206                build_pcurve_on_surface_range(
2207                    &surface,
2208                    &edge.curve,
2209                    edge.t0,
2210                    edge.t1,
2211                    coedge.forward,
2212                    tolerance,
2213                ).or_refuse(KernelStage::Sew, "build_pcurve_on_surface_range")?
2214            } else {
2215                let mut pcurve = build_pcurve_on_surface(&surface, &edge.curve).or_refuse(KernelStage::Sew, "build_pcurve_on_surface")?;
2216                if !coedge.forward {
2217                    pcurve = pcurve.reversed().or_refuse(KernelStage::Sew, "reversed")?;
2218                }
2219                pcurve
2220            };
2221            coedge.pcurve = reanchor_pcurve_u(&pcurve, &coedge.pcurve, u_period)?;
2222        }
2223    }
2224    Ok(())
2225}
2226
2227/// The curve in `curves` whose midpoint is nearest `reference` (branch selection
2228/// for a surface∩surface intersection that returns more than one component).
2229fn nearest_curve(curves: &[NurbsCurve], reference: Vec3) -> Result<NurbsCurve, KernelRefusal> {
2230    let mut best: Option<(f64, &NurbsCurve)> = None;
2231    for curve in curves {
2232        let mid = curve.evaluate(0.5).or_refuse(KernelStage::Refine, "evaluate")?;
2233        let d = mid.sub(reference).length();
2234        if best.map(|(best_d, _)| d < best_d).unwrap_or(true) {
2235            best = Some((d, curve));
2236        }
2237    }
2238    best.map(|(_, curve)| curve.clone())
2239        .ok_or_else(|| {
2240            KernelRefusal::internal(
2241                KernelStage::Refine,
2242                "empty_intersection",
2243                "offset_ruled_face: empty intersection",
2244            )
2245        })
2246}
2247