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