Skip to main content

brep_kernel/blending/blend/edge/
closed.rs

1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4/// General closed-edge rolling-ball fillet or chamfer by direct §6.9 topology
5/// surgery. The refusal class of a concave closed rim whose fill overruns the
6/// plane it stands on (lane L, 2026-10-16).
7pub(crate) const CONCAVE_RIM_OVERRUN_WHAT: &str = "concave_rim_overrun";
8
9/// Lane L (2026-10-16): a CONCAVE closed rim standing on a PLANE — the rim one of
10/// the plane's INNER loops (a boss on a plate) — whose fill reaches past the
11/// plane's OUTER loop is not a face consumed up to one preserved far edge: the
12/// fill overruns the plate, and the body owed is that fill clipped by the plate's
13/// neighbouring faces. The edge-preserving construction found four "far" edges
14/// there and said "ambiguous preserved boundary edge". Read geometrically: the rim
15/// sampled, each sample stepped `radius` into the plane square to the rim (the
16/// fill's contact for a boss standing square to the plane), tested against the
17/// outer loop as a polygon in the plane (the chart clamps; the loop does not).
18/// `BREP_CONCAVE_RIM_TRUE_CONTACTS` (default on; `=0` restores the estimate): the
19/// overrun test reads the march's own contacts on the plate where it has them, not
20/// a point a whole radius from the rim. The radius is the contact's reach only
21/// where the wall stands square to the plate; against a ball (a ball standing on a
22/// plate, its rim rounded by 3) the contact lies nearer, at `√((R + r)² − r²) −
23/// R`, 2.416 against 3, and the fill that fits was refused.
24fn concave_rim_true_contacts_on() -> bool {
25    switch!("BREP_CONCAVE_RIM_TRUE_CONTACTS", true)
26}
27
28fn concave_rim_overrun(
29    solid: &BrepSolid,
30    edge: &EdgeRecord,
31    mates: [(&FaceRecord, usize); 2],
32    radius: f64,
33    contacts: Option<[Vec<Vec3>; 2]>,
34) -> Result<Option<KernelRefusal>, KernelRefusal> {
35    const RIM_SAMPLES: usize = 64;
36    const LOOP_SAMPLES: usize = 256;
37    if edge.start_vertex_id != edge.end_vertex_id {
38        return Ok(None);
39    }
40    for (index, (face, rim_loop)) in mates.iter().copied().enumerate() {
41        if rim_loop == 0 {
42            continue;
43        }
44        let Some(crate::AnalyticSurface::Plane { origin, u_dir, v_dir, .. }) = face.surface.analytic() else { continue };
45        let wall = mates[1 - index].0;
46        let e1 = u_dir.normalized().or_refuse(KernelStage::Classify, "normalized")?;
47        let normal = u_dir.cross(*v_dir).normalized().or_refuse(KernelStage::Classify, "normalized")?;
48        let e2 = normal.cross(e1);
49        let flat = |p: Vec3| -> (f64, f64) { let d = p.sub(*origin); (d.dot(e1), d.dot(e2)) };
50        // The outer loop and the rim's own loop as polygons in the plane.
51        let polygon = |loop_index: usize| -> Result<Vec<(f64, f64)>, KernelRefusal> {
52            let mut points: Vec<(f64, f64)> = Vec::new();
53            for coedge in &face.loops[loop_index].coedges {
54                let Some(boundary) = solid.edges.iter().find(|candidate| candidate.id == coedge.edge_id) else { continue };
55                for k in 0..LOOP_SAMPLES {
56                    let f = k as f64 / LOOP_SAMPLES as f64;
57                    let t = if coedge.forward { boundary.t0 + (boundary.t1 - boundary.t0) * f } else { boundary.t1 - (boundary.t1 - boundary.t0) * f };
58                    points.push(flat(boundary.curve.evaluate(t).or_refuse(KernelStage::Classify, "evaluate")?));
59                }
60            }
61            Ok(points)
62        };
63        let (outer, rim_polygon) = (polygon(0)?, polygon(rim_loop)?);
64        let within = |points: &[(f64, f64)], x: f64, y: f64| -> bool {
65            crate::polygon::parity_inside([x, y], points.len(), |k| [points[k].0, points[k].1])
66        };
67        // The plate's region about the rim: inside the outer loop, outside the
68        // rim's own loop (where the boss stands).
69        let inside = |x: f64, y: f64| -> bool { within(&outer, x, y) && !within(&rim_polygon, x, y) };
70        // Concave: at a rim sample the wall's OUTWARD normal points into the
71        // plate's region (a boss facing out over its plate); a hole's wall
72        // faces its axis (convex) and is not this class.
73        {
74            let derivatives = edge.curve.derivatives(edge.t0, 1).or_refuse(KernelStage::Classify, "derivatives")?;
75            let (point, tangent) = (derivatives[0], derivatives[1]);
76            let side = normal.cross(tangent).normalized().or_refuse(KernelStage::Classify, "normalized")?;
77            // A twentieth of the radius: well past the sampled loops' chord
78            // sag, well inside the fill's own reach.
79            let probe = radius * 0.05;
80            let (px, py) = flat(point.add(side.scale(probe)));
81            let toward = if inside(px, py) { side } else { side.scale(-1.0) };
82            let foot = crate::project_point_to_surface(&wall.surface, point).or_refuse(KernelStage::Classify, "project_point_to_surface")?;
83            let wall_normal = wall.surface.normal(foot.u, foot.v).or_refuse(KernelStage::Classify, "normal")?;
84            let outward = if wall.same_sense { wall_normal } else { wall_normal.scale(-1.0) };
85            if outward.dot(toward) <= 0.0 {
86                continue;
87            }
88        }
89        let mut overruns = false;
90        if let (true, Some(contacts)) = (concave_rim_true_contacts_on(), contacts.as_ref()) {
91            // A contact ON the outer loop (a fill that just touches the
92            // plate's edge, tangent to it) has not overrun it: the parity
93            // test cannot say which side such a point is on, so its distance
94            // to the loop decides.
95            let on_loop = |x: f64, y: f64| -> bool {
96                let band = 1e-7 * (1.0 + x.abs() + y.abs());
97                (0..outer.len()).any(|k| {
98                    let (a, b) = (outer[k], outer[(k + 1) % outer.len()]);
99                    let (dx, dy) = (b.0 - a.0, b.1 - a.1);
100                    let length2 = dx * dx + dy * dy;
101                    let t = if length2 > 0.0 { (((x - a.0) * dx + (y - a.1) * dy) / length2).clamp(0.0, 1.0) } else { 0.0 };
102                    (x - a.0 - t * dx).hypot(y - a.1 - t * dy) <= band
103                })
104            };
105            for contact in &contacts[index] {
106                let (x, y) = flat(*contact);
107                if !within(&outer, x, y) && !on_loop(x, y) {
108                    overruns = true;
109                    break;
110                }
111            }
112        } else {
113        for k in 0..RIM_SAMPLES {
114            let t = edge.t0 + (edge.t1 - edge.t0) * (k as f64 + 0.5) / RIM_SAMPLES as f64;
115            let derivatives = edge.curve.derivatives(t, 1).or_refuse(KernelStage::Classify, "derivatives")?;
116            let (point, tangent) = (derivatives[0], derivatives[1]);
117            let Ok(side) = normal.cross(tangent).normalized() else { continue };
118            // Into the plate: of the two in-plane normals to the rim, the one a
119            // small step along which stays inside the outer loop's region.
120            // A twentieth of the radius: well past the sampled loops' chord
121            // sag, well inside the fill's own reach.
122            let probe = radius * 0.05;
123            let toward = if inside(flat(point.add(side.scale(probe))).0, flat(point.add(side.scale(probe))).1) { side } else { side.scale(-1.0) };
124            let contact = flat(point.add(toward.scale(radius)));
125            if !within(&outer, contact.0, contact.1) {
126                overruns = true;
127                break;
128            }
129        }
130        }
131        if overruns {
132            return Ok(Some(KernelRefusal::unsupported(KernelStage::Classify, CONCAVE_RIM_OVERRUN_WHAT, format!(
133                "blend: the concave rim fill overruns the face's outer boundary — rolling on rim edge {}, the ball's contact on the plane {} it stands on (radius {radius} from the rim) reaches past that plane's outer loop, so the fill must be clipped by the plane's neighbouring faces, which this lane does not build",
134                edge.id,
135                face.name.clone().unwrap_or_else(|| format!("{}", face.id)),
136            ))));
137        }
138    }
139    Ok(None)
140}
141
142pub fn blend_closed_edge(
143    solid: &BrepSolid,
144    edge_id: u64,
145    radius: f64,
146    chamfer: bool,
147    name: Option<&str>,
148) -> Result<BrepSolid, KernelRefusal> {
149    // One blend operation: the reports of an earlier one, never consumed, are
150    // dropped as it starts (nested calls keep this operation's).
151    let _operation = crate::blend::BlendOperation::enter();
152    if !(radius > 0.0) || !radius.is_finite() {
153        return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
154    }
155    blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name)
156}
157
158/// [`blend_closed_edge`] with the closed-rim support refit's KNOT-AWARE retry
159/// (lane L, 2026-10-16): where a support pcurve's uniform track misses the floor,
160/// the fit is retried on a track that also samples the rail's own knots
161/// (`project_piece_pcurve_within_knot_retry`). Only for a caller whose refusal is
162/// TERMINAL for the whole operation — the fillet tool's last rung
163/// (`fillet/tool.rs`, `lcount201_knot_aware_last_rung_20261016`) on a refusal
164/// [`is_pcurve_floor_refusal`] reads: roads that call `blend_closed_edge` per
165/// rung and recover a floor miss themselves (the stadium, half-torus and box
166/// ladders) never see it.
167pub fn blend_closed_edge_knot_aware(
168    solid: &BrepSolid,
169    edge_id: u64,
170    radius: f64,
171    chamfer: bool,
172    name: Option<&str>,
173) -> Result<BrepSolid, KernelRefusal> {
174    let _operation = crate::blend::BlendOperation::enter();
175    if !(radius > 0.0) || !radius.is_finite() {
176        return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
177    }
178    KNOT_RETRY.with(|flag| flag.set(true));
179    let built = blend_closed_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name);
180    KNOT_RETRY.with(|flag| flag.set(false));
181    built
182}
183
184/// A refusal for a support pcurve that does not reach the refinement floor —
185/// read off its CLASS and code (`CHAIN_PCURVE_FLOOR_WHAT`,
186/// `CLOSED_SUPPORT_PCURVE_FLOOR_WHAT`), never its wording.
187pub fn is_pcurve_floor_refusal(refusal: &KernelRefusal) -> bool {
188    matches!(&refusal.class, crate::RefusalClass::NonConvergence { what }
189        if what == crate::blend::chain::CHAIN_PCURVE_FLOOR_WHAT || what == CLOSED_SUPPORT_PCURVE_FLOOR_WHAT)
190}
191
192/// The code of a closed-rim support refit that misses the floor as shipped.
193pub(crate) const CLOSED_SUPPORT_PCURVE_FLOOR_WHAT: &str = "closed_support_pcurve_floor";
194
195/// Variable-radius blend (4.9.5): radius stops as (edge fraction, radius)
196/// pairs, linearly interpolated along the edge parameter and clamped at
197/// the ends.  Closed edges must supply matching first/last radii.
198pub fn blend_edge_variable(
199    solid: &BrepSolid,
200    edge_id: u64,
201    radii: &[(f64, f64)],
202    chamfer: bool,
203    name: Option<&str>,
204) -> Result<BrepSolid, KernelRefusal> {
205    // One blend operation: the reports of an earlier one, never consumed, are
206    // dropped as it starts (nested calls keep this operation's).
207    let _operation = crate::blend::BlendOperation::enter();
208    if radii.is_empty() || radii.iter().any(|(_, radius)| !(*radius > 0.0)) {
209        return Err(KernelRefusal::input(KernelStage::Collect, "radius_stop", "blend: every radius stop must be positive"));
210    }
211    let mut stops = radii.to_vec();
212    stops.sort_by(|a, b| a.0.total_cmp(&b.0));
213    let edge = solid
214        .edges
215        .iter()
216        .find(|edge| edge.id == edge_id)
217        .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
218    let closed = edge.start_vertex_id == edge.end_vertex_id;
219    if closed && (stops[0].1 - stops[stops.len() - 1].1).abs() > 1e-12 {
220        return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge_radii", "blend: closed edges need equal first/last radii"));
221    }
222    // CONSTANT stops must DEGENERATE to the exact constant-radius
223    // machinery (§6.9 exact cylinder cutters where the mates allow them,
224    // the same general march otherwise).  The general march's fitted
225    // NURBS is geometrically exact for a constant radius, but downstream
226    // booleans treat it as a generic surface: marched SSI curves against
227    // a tangent mate drift O(√ε) along the tangency (measured 1.2e-3 on
228    // a 10-box chain miter), while the exact representation intersects
229    // analytically — this is what keeps chain-corner miters composable.
230    let first_radius = stops[0].1;
231    if stops
232        .iter()
233        .all(|(_, radius)| (radius - first_radius).abs() <= 1e-12 * (1.0 + first_radius.abs()))
234    {
235        return if chamfer {
236            crate::fillet::chamfer_edge(solid, edge_id, first_radius, name)
237        } else {
238            crate::fillet::fillet_edge(solid, edge_id, first_radius, name)
239        };
240    }
241    let (t0, t1) = (edge.t0, edge.t1);
242    let radius_at = move |t: f64| -> f64 {
243        let raw = (t - t0) / (t1 - t0);
244        // Closed edges WRAP (the anchored march may start mid-edge and run
245        // past t1 once around — clamping misplaced mid-profile stops
246        // there); open edges CLAMP flat past the ends, which is only the
247        // marched overshoot region beyond the trims.  The blend-end
248        // stations are pinned exactly at t0/t1 by the open march, so the
249        // profile kink the clamp creates sits ON an interpolation node and
250        // the fitted rows still pass through the closed-form end stations.
251        let fraction = if closed {
252            raw.rem_euclid(1.0)
253        } else {
254            raw.clamp(0.0, 1.0)
255        };
256        if fraction <= stops[0].0 {
257            return stops[0].1;
258        }
259        for pair in stops.windows(2) {
260            if fraction <= pair[1].0 {
261                let width = (pair[1].0 - pair[0].0).max(1e-12);
262                let local = (fraction - pair[0].0) / width;
263                return pair[0].1 + (pair[1].1 - pair[0].1) * local;
264            }
265        }
266        stops[stops.len() - 1].1
267    };
268    // A VARIABLE profile (constant stops left above for the constant entry):
269    // no constant-radius provenance, so no rolling-ball wall verdict.
270    if closed {
271        blend_closed_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
272    } else {
273        blend_open_edge_impl(solid, edge_id, &radius_at, None, chamfer, name)
274    }
275}
276
277/// `constant`: the radius when the blend came in through a CONSTANT-radius
278/// entry point (`blend_closed_edge`), `None` for a variable profile — the
279/// only provenance on which the wall is judged against one rolling ball.
280fn blend_closed_edge_impl(
281    solid: &BrepSolid,
282    edge_id: u64,
283    radius_at: &dyn Fn(f64) -> f64,
284    constant: Option<f64>,
285    chamfer: bool,
286    name: Option<&str>,
287) -> Result<BrepSolid, KernelRefusal> {
288    let _closed_timing = crate::blend::BlendTimer::start("timing.closed_rim_ms");
289    let prepare_timing = crate::blend::BlendTimer::start("timing.closed.prepare_ms");
290    if let Some(separated) = separate_spherical_domain_cut(solid, edge_id)? {
291        return blend_closed_edge_impl(&separated, edge_id, radius_at, constant, chamfer, name);
292    }
293    if let Some(simplified) = remove_retraced_cuts(solid, edge_id)? {
294        return blend_closed_edge_impl(&simplified, edge_id, radius_at, constant, chamfer, name);
295    }
296    let edge = solid
297        .edges
298        .iter()
299        .find(|edge| edge.id == edge_id)
300        .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
301    if edge.start_vertex_id != edge.end_vertex_id {
302        return Err(KernelRefusal::input(KernelStage::Collect, "closed_edge", "blend: general path currently requires a CLOSED edge"));
303    }
304    let (face_a, loop_a, coedge_a) = locate_mate(solid, edge_id, None)?;
305    let (face_b, loop_b, coedge_b) = locate_mate(solid, edge_id, Some((face_a.id, loop_a)))?;
306    if face_a.id == face_b.id && loop_a == loop_b {
307        return Err(KernelRefusal::unsupported(KernelStage::Classify, "seam_edge", "blend: edge is used twice by one loop (seam edge?)"));
308    }
309
310    // Seam-structured loops force the blend seam onto that carrier's seam
311    // meridian; at most one mate may need it.
312    let seam_a = face_a.loops[loop_a].coedges.len() > 1;
313    let seam_b = face_b.loops[loop_b].coedges.len() > 1;
314    let (first_face, first_loop, first_coedge, second_face, second_loop, second_coedge) =
315        if seam_a || !seam_b {
316            (face_a, loop_a, coedge_a, face_b, loop_b, coedge_b)
317        } else {
318            (face_b, loop_b, coedge_b, face_a, loop_a, coedge_a)
319        };
320    let anchored = first_face.loops[first_loop].coedges.len() > 1;
321    let second_seam = anchored && second_face.loops[second_loop].coedges.len() > 1;
322    let anchor_u = if anchored {
323        // Lock onto the carrier's seam meridian: the u-domain start.
324        Some(first_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0])
325    } else {
326        None
327    };
328
329    // Signed radii from the cross-section seed at the edge midpoint.
330    let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
331    let (rho1, rho2) = signed_radii(
332        edge,
333        first_face,
334        first_coedge,
335        second_face,
336        second_coedge,
337        mid_radius,
338    )?;
339
340    let first_mate = BlendMate {
341        face: first_face,
342        coedge: first_coedge,
343        loop_index: first_loop,
344        rho: rho1,
345    };
346    let second_mate = BlendMate {
347        face: second_face,
348        coedge: second_coedge,
349        loop_index: second_loop,
350        rho: rho2,
351    };
352    let mid_t = edge.t0 + (edge.t1 - edge.t0) * 0.5;
353    // Anchored FIRST, always — everything that already works takes this rung
354    // and is unchanged.  An anchored march freezes the first carrier's u on
355    // its seam meridian and frees t (`solve_anchored_start`), so it is only
356    // solvable when the edge actually reaches that meridian.  A closed edge
357    // cut by a plane through a surface of revolution's AXIS is one whole
358    // MERIDIAN of that carrier — constant u, a full turn in v — so no station
359    // on it can sit at the seam, and the seam may even have been trimmed off
360    // the face (the 2026-09-01 reported document: the kept torus was
361    // u ∈ [0.1369, 0.8869] while the anchor locked u = 0).  That march reports
362    // "anchored seam start did not converge"; the UNANCHORED march solves the
363    // same edge exactly, because the edge's own endpoints already sit on the
364    // carrier's OTHER (v) seam.  Retrying is what makes this a rung of the
365    // existing ladder rather than a predicate with a tolerance to tune: a
366    // march that needs its anchor still gets one.
367    use crate::blend::chain::FoldPolicy;
368    let march_with = |fold: FoldPolicy, depth_budget: usize| -> Result<Vec<Station>, KernelRefusal> {
369        match march_stations(edge, &first_mate, &second_mate, radius_at, anchor_u, fold, depth_budget) {
370            Ok(stations) => Ok(stations),
371            Err(anchored_error) if anchor_u.is_some() => {
372                march_stations(edge, &first_mate, &second_mate, radius_at, None, fold, depth_budget).map_err(
373                    |retry_error| {
374                        if crate::blend::is_wall_fold(&retry_error)
375                        || crate::blend::is_marched_fit_off_carriers(&retry_error) {
376                            retry_error
377                        } else {
378                            anchored_error
379                        }
380                    },
381                )
382            }
383            Err(anchored_error) => Err(anchored_error),
384        }
385    };
386    let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
387    // A FILLET of one constant radius is also judged on its wall between the
388    // rails, against that ball's sweep, at the unchanged `intersection_fit`
389    // (`ClosedWallCheck`); a chamfer or a varying radius is not.
390    let wall_check = {
391        // Constant-radius PROVENANCE (the entry point), never sampled
392        // constancy: a variable profile can agree at any finite set of samples.
393        let radius = constant.unwrap_or(f64::NAN);
394        let constant = constant.is_some();
395        // The construction request as on the closed chain's plain fillet
396        // rungs: `KernelTolerances::model` on the wall between the rails.
397        let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
398        let bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
399        (!chamfer && constant && radius.is_finite() && radius != 0.0).then(|| crate::blend::stations::ClosedWallCheck {
400            radius: radius.abs(),
401            bar,
402            model: model.is_finite().then_some(model),
403        })
404    };
405    crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.set(None));
406    crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.set(None));
407    prepare_timing.stop();
408    let exact_revolution = crate::blend::exact_rim_lane_on()
409        && crate::blend::stations::closed_revolution_axis(&first_mate, &second_mate).is_some();
410    let depth_budget = if exact_revolution { 0 } else { MAX_REFINEMENT_DEPTH };
411    if exact_revolution {
412        crate::blend::record_count("count.closed.exact_lane", 1);
413    }
414    let march_timing = crate::blend::BlendTimer::start("timing.closed.march_ms");
415    let attempt_timing = crate::blend::BlendTimer::start("timing.closed.march.anchored_fallback_ms");
416    // An asymmetric chamfer's stations come from its setback law
417    // (`blend/setback.rs`): no analytic rim (those solve the rolling ball),
418    // and a station that does not solve is the answer, not a cue for the
419    // edge-preserving construction, which builds from the radius.
420    let setback = crate::blend::setback::active();
421    let analytic = if setback.is_some() {
422        None
423    } else {
424        crate::blend::stations::analytic_rim::construct(edge, &first_mate, &second_mate, constant, chamfer, anchor_u)
425    };
426    let setback_stations = match &setback {
427        Some(law) => Some(crate::blend::setback::march_closed(solid, edge, &first_mate, &second_mate, law, anchor_u, rail_bar)?),
428        None => None,
429    };
430    let (analytic_stations, analytic_rows) = match analytic {
431        Some((stations, rows)) => (Some(stations), Some(rows)),
432        None => (None, None),
433    };
434    let analytic_stations = setback_stations.or(analytic_stations);
435    let marched = match analytic_stations.map(Ok).unwrap_or_else(|| march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, anchor_u, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check)) {
436        Ok(stations) => {
437            std::mem::forget(attempt_timing);
438            Ok(stations)
439        }
440        Err(failure) => {
441            // Post-refinement errors return directly, before either the
442            // unanchored retry or the legacy edge-preserving construction.
443            let anchored_error = failure.into_initial()?;
444            if anchor_u.is_some() {
445                attempt_timing.stop();
446                crate::blend::record_count("count.closed.march.anchor_fallback", 1);
447                if std::env::var("BREP_DEBUG_CLOSED_RAILS").is_ok() {
448                    eprintln!("closed initial anchor fallback: edge {}: {anchored_error}", edge.id);
449                }
450                match march_stations_with_rail_bar(edge, &first_mate, &second_mate, radius_at, None, FoldPolicy::Refuse, depth_budget, rail_bar, wall_check) {
451                    Ok(stations) => Ok(stations),
452                    Err(failure) => {
453                        let retry_error = failure.into_initial()?;
454                        if crate::blend::is_wall_fold(&retry_error) { Err(retry_error) }
455                        else { Err(anchored_error) }
456                    }
457                }
458            } else { Err(anchored_error) }
459        }
460    };
461    // A wall that FOLDS through itself: the lens of the envelope inside the
462    // swept ball volume is CARVED (`blend/carve.rs`), the way the closed chain
463    // lane carves it, and the crease sewn as an inner loop of the blend face.
464    // A fold that reaches a RAIL is not a lens (nothing to keep on one side)
465    // and stays the named refusal; so does a chamfer's (a straight section
466    // has no ball centre to read the fold locus against).  The march is
467    // re-run under `FoldPolicy::Carve` so the folding wall is built; the lane
468    // then checks the fitted rows are accurate enough to carve and that the
469    // lens does not straddle the wall's own seam before anything is cut.
470    // The march's unmet construction request (if any), taken before any other
471    // march runs; it is reported only for the plain wall this lane builds
472    // from that march's stations.
473    let model_unmet = crate::blend::stations::CLOSED_MODEL_UNMET.with(|slot| slot.take());
474    let request_rails = crate::blend::stations::CLOSED_REQUEST_RAILS.with(|slot| slot.take());
475    let mut carve_fold = false;
476    // The CARVE LADDER (see below): the stations a folding wall is carved
477    // from, or the refusal that there is no wall accurate enough to carve.
478    // Shared by the march's own fold verdict and by the rows-loop verdict
479    // read after the fit (2026-10-17: `fillet-mouth-over-wall-bore-5p5-r2p2`'s
480    // F2 wall loops at the rim's midpoint without the march's centre-curve
481    // verdict ever firing).
482    let climb_carve_ladder = |error: &KernelRefusal| -> Result<Vec<Station>, KernelRefusal> {
483        let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
484        let mut climbed: Option<Vec<Station>> = None;
485        let mut last_off = f64::NAN;
486        for depth_budget in MAX_REFINEMENT_DEPTH..=CARVE_MAX_REFINEMENT_DEPTH {
487            let stations = match march_with(FoldPolicy::Carve, depth_budget) {
488                Ok(stations) => stations,
489                // The Carve march did not build: the fold stays the answer.
490                Err(_) => break,
491            };
492            let parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
493            let off = match fit_closed_rows(&stations, &parameters, chamfer) {
494                Ok(rows) => rails_off_closed_carriers(&rows, &stations, &parameters, &first_mate, &second_mate)?,
495                Err(_) => break,
496            };
497            last_off = off;
498            crate::blend::carve::carve_trace(format_args!(
499                "blend closed-edge carve ladder: depth budget {depth_budget} ({} stations) leaves the rails {off:.3e} off their carriers (bar {rail_bar:.3e})",
500                stations.len()
501            ));
502            if off <= rail_bar {
503                climbed = Some(stations);
504                break;
505            }
506        }
507        climbed.ok_or_else(|| {
508            crate::blend::fold::wall_fold_refusal(
509                Vec::new(),
510                format!(
511                    "{} {} fits this edge: the wall folds, and at depth budget {CARVE_MAX_REFINEMENT_DEPTH} — the top of the closed march's carve ladder — its fitted rails still stand {last_off:.3e} off their carriers between stations against a bar of {rail_bar:.3e}, so there is no wall accurate enough to carve ({error})",
512                    crate::blend::WALL_FOLDS,
513                    radius_at(mid_t).abs()
514                ),
515            )
516        })
517    };
518    let marched = match marched {
519        Err(error)
520            if crate::blend::is_wall_fold(&error)
521                && !crate::blend::fold::is_rail_fold(&error)
522                && !chamfer =>
523        {
524            crate::blend::carve::carve_trace(format_args!(
525                "blend closed-edge: the wall folds ({error}); re-marching under Carve"
526            ));
527            match climb_carve_ladder(&error) {
528                Ok(stations) => {
529                    carve_fold = true;
530                    Ok(stations)
531                }
532                Err(refusal) => Err(refusal),
533            }
534        }
535        other => other,
536    };
537    let mut stations = match marched {
538        Ok(stations) => stations,
539        // A wall that FOLDS is refused here for the same reason an escaped
540        // march is: the edge-preserving construction would build a blend on the
541        // same folding centre curve, and if it converged it would return the
542        // very surface this refusal exists to stop.  It is not the fallback's
543        // question to answer, so the geometry is reported as-is.
544        Err(march_error) if crate::blend::is_wall_fold(&march_error)
545            || crate::blend::is_marched_fit_off_carriers(&march_error) => return Err(march_error),
546        Err(march_error) if crate::blend::is_ball_off_carrier(&march_error) => {
547            // An ESCAPED march is not a non-convergence: the tangency system
548            // had no solution at all, so no fallback can build the blend the
549            // rolling ball never made.  Report the escape itself — routing it
550            // to the edge-preserving construction below only reports THAT
551            // lane's complaint about a face it was never meant to consume
552            // ("ambiguous preserved boundary edge" on the 2026-09-10 report).
553            return Err(march_error);
554        }
555        Err(march_error) => {
556            // Lane L (2026-10-16): a concave rim overrunning its plane is
557            // named before the edge-preserving construction misreads it.
558            if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t), None)? {
559                return Err(refusal);
560            }
561            // A non-converging march often means the rolling ball falls off
562            // one face — try the edge-preserving construction (4.9.7).
563            return blend_closed_edge_keep(
564                solid,
565                edge,
566                &first_mate,
567                &second_mate,
568                radius_at(mid_t),
569                rho1,
570                chamfer,
571                name,
572            )
573            .or_else(|_| {
574                blend_closed_edge_keep(
575                    solid,
576                    edge,
577                    &second_mate,
578                    &first_mate,
579                    radius_at(mid_t),
580                    rho2,
581                    chamfer,
582                    name,
583                )
584            })
585            .map_err(|keep_error| {
586                format!("{march_error}; edge-preserving blend also failed: {keep_error}")
587            }).or_refuse(KernelStage::Refine, "map_err");
588        }
589    };
590    // Support-out-of-trim detection (4.9.7 trigger): a tangency track that
591    // leaves its face's trimmed region cannot be trimmed there — the far
592    // boundary must be PRESERVED instead.
593    let support_exits = |face: &FaceRecord, second_side: bool| -> bool {
594        // Adaptive marches vary the station count; keep at least the old fixed
595        // grid's ~16-probe floor while inheriting extra density where the
596        // march refined (which is where an exit would hide).
597        let stride = (stations.len() / 16).max(1);
598        stations.iter().step_by(stride).any(|station| {
599            support_exits_at(face, if second_side { station.uv2 } else { station.uv1 })
600        })
601    };
602    // The edge-preserving construction below cannot answer it (it consumes
603    // the mate up to one preserved far boundary; a rolled track has none) and
604    // on the reported bore mouth it said "ambiguous preserved boundary edge"
605    // about a face it was never meant to consume. The crossings are located
606    // and the lane declines BY NAME; the ladder falls through to the cutter
607    // as before. An exit across a sharp boundary, or onto a tangent neighbour
608    // the ball cannot roll onto, keeps the route below unchanged.
609    let exits_timing = crate::blend::BlendTimer::start("timing.closed.march.exits_ms");
610    // A mate bounded only by parallels of the rim's axis answers for all its
611    // stations with ONE probe (`support_exit_between_parallels`); any other
612    // mate, and any station near a boundary, takes the full probe scan.
613    let revolution = crate::blend::stations::closed_revolution_axis(&first_mate, &second_mate);
614    let exit_of = |face: &FaceRecord, second_side: bool| -> bool {
615        let contacts: Vec<Vec3> = stations.iter().map(|station| if second_side { station.p2 } else { station.p1 }).collect();
616        let probe = |station: usize| -> bool {
617            support_exits_at(face, if second_side { stations[station].uv2 } else { stations[station].uv1 })
618        };
619        match revolution.and_then(|axis| support_exit_between_parallels(solid, face, axis, &contacts, &probe)) {
620            Some(exit) => {
621                crate::blend::record_count("count.closed.exits_by_parallels", 1);
622                exit
623            }
624            None => support_exits(face, second_side),
625        }
626    };
627    let exits = [exit_of(first_face, false), exit_of(second_face, true)];
628    exits_timing.stop();
629    // The rollover and edge-preserving lanes below build from the ball's
630    // radius: a setback rail that leaves its face is refused here by name.
631    if setback.is_some() && (exits[0] || exits[1]) {
632        return Err(crate::blend::setback::unsupported(
633            "setback_rail_leaves_face",
634            format!(
635                "asymmetric chamfer: the rail on face {} runs off that face",
636                if exits[0] { first_face.id } else { second_face.id }
637            ),
638        ));
639    }
640    if exits[0] || exits[1] {
641        let scale = crate::blend::stations::march_model_scale(&edge.curve, edge.t0, edge.t1, radius_at(mid_t))?;
642        match crate::blend::rollover::plan_closed(solid, edge, &stations, [&first_mate, &second_mate], radius_at, scale) {
643            Ok(Some(plan)) => {
644                // The BUILD (behind its switch): every piece marched on its
645                // own carriers and fitted, then sewn on the crossing ribs.
646                if crate::blend::rollover_on() {
647                    let pieces = crate::blend::rollover::march_pieces(
648                        solid, edge, &plan, &stations, [&first_mate, &second_mate], radius_at, scale, rail_bar, chamfer, anchor_u,
649                    )?;
650                    let built = crate::blend::rollover::build(solid, edge, [&first_mate, &second_mate], &plan, pieces, anchor_u, name)?;
651                    march_timing.stop();
652                    return Ok(built);
653                }
654                return Err(crate::blend::rollover::refusal(&plan, radius_at(mid_t).abs()));
655            }
656            Ok(None) => {}
657            Err(error) => {
658                if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
659                    eprintln!("closed-rim rollover scan did not read the exit: {error}");
660                }
661            }
662        }
663    }
664    if setback.is_none() {
665        let contacts = [
666            stations.iter().map(|station| station.p1).collect::<Vec<_>>(),
667            stations.iter().map(|station| station.p2).collect::<Vec<_>>(),
668        ];
669        if let Some(refusal) = concave_rim_overrun(solid, edge, [(first_face, first_loop), (second_face, second_loop)], radius_at(mid_t), Some(contacts))? {
670            return Err(refusal);
671        }
672    }
673    if exits[1] {
674        return blend_closed_edge_keep(
675            solid,
676            edge,
677            &first_mate,
678            &second_mate,
679            radius_at(mid_t),
680            rho1,
681            chamfer,
682            name,
683        );
684    }
685    if exits[0] {
686        return blend_closed_edge_keep(
687            solid,
688            edge,
689            &second_mate,
690            &first_mate,
691            radius_at(mid_t),
692            rho2,
693            chamfer,
694            name,
695        );
696    }
697    if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
698        let last = stations.len() - 1;
699        for index in [
700            0usize,
701            1.min(last),
702            2.min(last),
703            last.saturating_sub(1),
704            last,
705        ] {
706            let station = &stations[index];
707            eprintln!(
708                "station {index}: uv1=({:.6},{:.6}) uv2=({:.6},{:.6}) p1=({:.6},{:.6},{:.6}) w={:.6}",
709                station.uv1[0], station.uv1[1], station.uv2[0], station.uv2[1],
710                station.p1.x, station.p1.y, station.p1.z, station.weight,
711            );
712        }
713    }
714    march_timing.stop();
715    crate::blend::record_count("count.closed.stations", stations.len() as u64);
716    let rows_timing = crate::blend::BlendTimer::start("timing.closed.rows_ms");
717    let mut parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
718    let exact_rows = analytic_rows.map(Ok).or_else(|| exact_closed_revolution_rows(&stations, &first_mate, &second_mate, chamfer));
719    crate::blend::record_count(
720        if exact_rows.is_some() { "count.rows.exact_revolution" } else { "count.rows.fitted_closed" },
721        1,
722    );
723    let rows = match exact_rows {
724        Some(rows) => rows?,
725        None => {
726            // The support pcurves interpolate the stations' (u, v) and are
727            // read nowhere between them: on the 20-degree crossing's single
728            // closed exit edge they stood 1.01e-4 (thin carrier) and
729            // 2.6e-5 to 3.8e-5 (fat carrier) off the rails they trim, while the
730            // rails stood on their carriers to 1e-6 — the whole of a 5e-6 to
731            // 1.1e-5 shell vector-area residual and of a seam-placement-
732            // dependent volume (dumps of c5ea8fb80 and 635ef7630, read
733            // input-free). Each is refitted to its rail's own projected track
734            // at the pcurve floor, by the closed chain's support-piece fitter.
735            let fit_timing = crate::blend::BlendTimer::start("timing.closed.rows.fit_ms");
736            let mut rows = fit_closed_rows(&stations, &parameters, chamfer)?;
737            fit_timing.stop();
738            // THE ROWS-LOOP VERDICT (2026-10-17): the fitted wall itself may
739            // pass back through itself along a row where the march's
740            // centre-curve verdict never fired (`fold::check_rows_loop`;
741            // `fillet-mouth-over-wall-bore-5p5-r2p2`'s F2 wall at the rim's
742            // midpoint). Such a wall is a folding wall, and takes the same
743            // road the march's own fold verdict takes: re-marched under
744            // Carve up the ladder, and its lens carved below.
745            if !carve_fold && !chamfer {
746                if let Err(error) = crate::blend::fold::check_rows_loop(
747                    &rows, &stations, &parameters, None, radius_at(mid_t).abs(), vec![first_face.id, second_face.id], crate::solid_scale(solid),
748                ) {
749                    crate::blend::carve::carve_trace(format_args!(
750                        "blend closed-edge: the fitted rows loop ({error}); re-marching under Carve"
751                    ));
752                    stations = climb_carve_ladder(&error)?;
753                    parameters = crate::blend::stations::closed_station_parameters(&stations, [&first_mate.face.surface, &second_mate.face.surface]);
754                    rows = fit_closed_rows(&stations, &parameters, chamfer)?;
755                    carve_fold = true;
756                }
757            }
758            // Lane G row 2 (behind `closed_trim_read_first_on`): each trim
759            // is first read as shipped at the floor and refitted only on a
760            // miss (`closed_support_trim`); off the switch, the refit as before.
761            let read_first = crate::blend::closed_trim_read_first_on();
762            let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_first_ms");
763            rows.cr_pcurve = if read_first {
764                closed_support_trim(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
765            } else {
766                refit_closed_support_pcurve(&rows.cr, &rows.cr_pcurve, &first_mate.face.surface)?
767            };
768            refit_timing.stop();
769            let refit_timing = crate::blend::BlendTimer::start("timing.closed.rows.refit_second_ms");
770            rows.cs_pcurve = if read_first {
771                closed_support_trim(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
772            } else {
773                refit_closed_support_pcurve(&rows.cs, &rows.cs_pcurve, &second_mate.face.surface)?
774            };
775            refit_timing.stop();
776            rows
777        }
778    };
779    rows_timing.stop();
780    let carve_timing = crate::blend::BlendTimer::start("timing.closed.carve_ms");
781    let crease = if carve_fold {
782        Some(carve_closed_wall(solid, &rows, &stations, &parameters, &first_mate, &second_mate, radius_at(mid_t).abs())?)
783    } else {
784        None
785    };
786    carve_timing.stop();
787    let surgery_timing = crate::blend::BlendTimer::start("timing.closed.surgery_ms");
788
789    // Both mates seam-structured: the second support crosses ITS carrier's
790    // seam meridian somewhere mid-loop; split cs there so face2's loop can
791    // keep its seam-in-one-loop structure.
792    let second_split = if second_seam {
793        let seam2 = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?[0];
794        let period2 = {
795            let [d0, d1] = second_face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
796            d1 - d0
797        };
798        // Bracket: unwrapped uv2 track crossing seam2 + k·period.
799        let u2_first = stations[0].uv2[0];
800        let u2_last = stations[stations.len() - 1].uv2[0];
801        let direction = (u2_last - u2_first).signum();
802        let mut k = ((u2_first - seam2) / period2).ceil();
803        if direction < 0.0 {
804            k = ((u2_first - seam2) / period2).floor();
805        }
806        let target = seam2 + k * period2;
807        let inside =
808            (target - u2_first) * direction > 1e-6 && (u2_last - target) * direction > 1e-6;
809        // ALIGNED seams (both carriers' meridians meet at the shared
810        // vertex — the natural coaxial construction): the crossing sits
811        // at the march boundary and no mid-loop split is needed; the
812        // blend seam vertex already lies on both meridians.
813        let aligned = (target - u2_first).abs() <= 1e-6
814            || (target - u2_last).abs() <= 1e-6
815            || ((u2_first - seam2) / period2).fract().abs() <= 1e-9;
816        if !inside && aligned {
817            None
818        } else if !inside {
819            return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing_bracket", "blend: second seam crossing not bracketed by the march"));
820        } else {
821            // Fit-space crossing parameter on the fitted cs (Newton via the
822            // fitted pcurve so the split lands exactly where the FITTED track
823            // crosses the meridian).
824            let [fit_low, fit_high] = rows.u_domain;
825            let mut p = fit_low
826                + (fit_high - fit_low) * {
827                    // Seed from the bracketing stations.
828                    let mut seed = 0.5;
829                    for pair in 0..stations.len() - 1 {
830                        let a = stations[pair].uv2[0];
831                        let b = stations[pair + 1].uv2[0];
832                        if (target - a) * (target - b) <= 0.0 {
833                            let local = (target - a) / (b - a);
834                            seed = (parameters[pair]
835                                + (parameters[pair + 1] - parameters[pair]) * local)
836                                .clamp(0.0, 1.0);
837                            break;
838                        }
839                    }
840                    seed
841                };
842            for _ in 0..NEWTON_ITERATIONS {
843                let value = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
844                if value.abs() <= 1e-12 {
845                    break;
846                }
847                let step = 1e-8;
848                let probed = rows.cs_pcurve.evaluate(p + step).or_refuse(KernelStage::Refine, "evaluate")?.x - target;
849                let derivative = (probed - value) / step;
850                if derivative.abs() <= 1e-14 {
851                    return Err(KernelRefusal::non_convergence(KernelStage::Refine, "seam_crossing_newton", "blend: second seam crossing Newton stalled"));
852                }
853                p -= value / derivative;
854            }
855            let crossing_v = rows.cs_pcurve.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?.y;
856            Some(SecondSeamSplit {
857                fit_parameter: p,
858                crossing_v,
859                period: period2 * direction,
860            })
861        }
862    } else {
863        None
864    };
865
866    // A wall that SHIPS short of its construction request says so, typed, on
867    // the blend face it built (exact name and surface); a carved wall or a
868    // fallback construction makes no request.
869    let model_unmet = if carve_fold { None } else { model_unmet };
870    let wall_surface = model_unmet.map(|_| rows.surface.clone());
871    let built = build_surgery(
872        solid,
873        edge,
874        &first_mate,
875        &second_mate,
876        rows,
877        second_split,
878        name,
879        crease,
880    )?;
881    surgery_timing.stop();
882    if let (Some((declared, reason, round, stations, ceiling)), Some(surface)) = (model_unmet, wall_surface) {
883        crate::blend::record_wall_model_report(crate::blend::WallModelReport {
884            name: name.map(str::to_string),
885            surface,
886            request: crate::KernelTolerances::for_solid(solid, 1e-7).model,
887            residual: declared,
888            // The request covers the rails (the shipped support curves) and
889            // the wall; the residual is the worse of the two.
890            detail: request_rails.map(|(rails, unread, wall)| {
891                format!("last request read: rails {rails:.3e} off their carriers at j/16 ({unread} interval(s) unread), wall {wall:.3e}")
892            }),
893            budget: crate::ApproximationBudget {
894                reason,
895                rounds_used: round,
896                rounds_limit: crate::blend::stations::REFINE_ROUNDS,
897                stations,
898                station_limit: ceiling,
899                mechanism: crate::BudgetMechanism::LocalRounds,
900                // The request covers rails and wall: which one the residual
901                // reads, and the rail intervals it could not read.
902                measured_component: match request_rails {
903                    Some((rails, _, wall)) if rails > wall => crate::MeasuredComponent::Rails,
904                    _ => crate::MeasuredComponent::Wall,
905                },
906                unread: request_rails.map_or(0, |(_, unread, _)| unread),
907            },
908        });
909    }
910    Ok(built)
911}
912
913/// A closed spherical hole may meet the carrier's full-domain seam at its
914/// loop vertex. The domain rectangle is only a parameter cut, so moving the
915/// hole need not move that cut. Separate the ring from the rectangle and give
916/// it its own coincident vertex before surgery. Unlike a retraced slit, the
917/// opposite seam branches and collapsed pole edges remain in the solid.
918fn separate_spherical_domain_cut(
919    solid: &BrepSolid,
920    edge_id: u64,
921) -> Result<Option<BrepSolid>, KernelRefusal> {
922    let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
923        return Ok(None);
924    };
925    if edge.start_vertex_id != edge.end_vertex_id {
926        return Ok(None);
927    }
928    let mut result = solid.clone();
929    let mut take_id = crate::blend::edge::fresh_id_source(solid);
930    let mut separated = false;
931    for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
932        let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
933            continue;
934        };
935        let [u0, u1] = face
936            .surface
937            .domain_u()
938            .or_refuse(KernelStage::Collect, "domain_cut_u")?;
939        let [v0, v1] = face
940            .surface
941            .domain_v()
942            .or_refuse(KernelStage::Collect, "domain_cut_v")?;
943        if [u0, u1, v0, v1] != [0.0, 1.0, 0.0, 1.0] {
944            continue;
945        }
946        if face
947            .surface
948            .closed_directions()
949            .or_refuse(KernelStage::Collect, "domain_cut_closed")?
950            != (true, false)
951        {
952            continue;
953        }
954        let band = 1e-8 * atlas.radius;
955        let bottom = face
956            .surface
957            .evaluate(u0, v0)
958            .or_refuse(KernelStage::Collect, "domain_cut_bottom")?;
959        let top = face
960            .surface
961            .evaluate(u0, v1)
962            .or_refuse(KernelStage::Collect, "domain_cut_top")?;
963        if bottom.add(top).scale(0.5).sub(atlas.centre).length() > band
964            || (bottom.sub(top).length() - 2.0 * atlas.radius).abs() > band
965        {
966            continue;
967        }
968        let mut additions = Vec::new();
969        let mut cut_ids = std::collections::HashSet::new();
970        for rim in &mut face.loops {
971            if rim.coedges.len() < 5 {
972                continue;
973            }
974            let positions: Vec<_> = rim
975                .coedges
976                .iter()
977                .enumerate()
978                .filter_map(|(i, c)| (c.edge_id == edge_id).then_some(i))
979                .collect();
980            if positions.len() != 1 {
981                continue;
982            }
983            let position = positions[0];
984            let ring = &rim.coedges[position];
985            let [a, b] = ring
986                .pcurve
987                .domain()
988                .or_refuse(KernelStage::Collect, "domain_cut_ring")?;
989            if ring
990                .pcurve
991                .evaluate(a)
992                .or_refuse(KernelStage::Collect, "domain_cut_ring_start")?
993                .sub(
994                    ring.pcurve
995                        .evaluate(b)
996                        .or_refuse(KernelStage::Collect, "domain_cut_ring_end")?,
997                )
998                .length()
999                > 1e-7
1000            {
1001                continue;
1002            }
1003            let rest: Vec<_> = rim
1004                .coedges
1005                .iter()
1006                .filter(|c| c.edge_id != edge_id)
1007                .collect();
1008            let mut points = Vec::new();
1009            let mut domain_cut = true;
1010            for coedge in &rest {
1011                let [a, b] = coedge
1012                    .pcurve
1013                    .domain()
1014                    .or_refuse(KernelStage::Collect, "domain_cut_trim")?;
1015                let start = coedge
1016                    .pcurve
1017                    .evaluate(a)
1018                    .or_refuse(KernelStage::Collect, "domain_cut_start")?;
1019                let end = coedge
1020                    .pcurve
1021                    .evaluate(b)
1022                    .or_refuse(KernelStage::Collect, "domain_cut_end")?;
1023                points.push(start);
1024                let degenerate = solid
1025                    .edges
1026                    .iter()
1027                    .any(|e| e.id == coedge.edge_id && e.degenerate);
1028                let at = |x: f64, low: f64, high: f64| {
1029                    (x - low).abs() <= 1e-8 || (x - high).abs() <= 1e-8
1030                };
1031                let on_boundary = if degenerate {
1032                    (start.y - end.y).abs() <= 1e-8
1033                        && at(start.y, v0, v1)
1034                        && coedge
1035                            .pcurve
1036                            .control_points
1037                            .iter()
1038                            .all(|p| (p.y / p.w - start.y).abs() <= 1e-8)
1039                } else {
1040                    at(start.x, u0, u1)
1041                        && (start.x - end.x).abs() <= 1e-8
1042                        && coedge
1043                            .pcurve
1044                            .control_points
1045                            .iter()
1046                            .all(|p| (p.x / p.w - start.x).abs() <= 1e-8)
1047                        && rest.iter().filter(|c| c.edge_id == coedge.edge_id).count() == 2
1048                        && rest
1049                            .iter()
1050                            .any(|c| c.edge_id == coedge.edge_id && c.forward != coedge.forward)
1051                };
1052                domain_cut &= on_boundary;
1053            }
1054            let twice_area: f64 = (0..points.len())
1055                .map(|i| {
1056                    let a = points[i];
1057                    let b = points[(i + 1) % points.len()];
1058                    a.x * b.y - a.y * b.x
1059                })
1060                .sum();
1061            let expected = (u1 - u0) * (v1 - v0) * if face.same_sense { 1.0 } else { -1.0 };
1062            if !domain_cut || (0.5 * twice_area - expected).abs() > 1e-7 * expected.abs() {
1063                continue;
1064            }
1065            // A relocated cut must belong only to this sphere. Its vertices
1066            // may touch the selected ring or collapsed poles, but no other
1067            // material edge. Its active parameter must already be sphere v.
1068            let cuts: std::collections::HashSet<_> = rest
1069                .iter()
1070                .filter_map(|c| {
1071                    solid
1072                        .edges
1073                        .iter()
1074                        .find(|e| e.id == c.edge_id && !e.degenerate)
1075                        .map(|e| e.id)
1076                })
1077                .collect();
1078            for cut in solid.edges.iter().filter(|e| cuts.contains(&e.id)) {
1079                let users = solid
1080                    .shells
1081                    .iter()
1082                    .flat_map(|s| &s.faces)
1083                    .flat_map(|f| &f.loops)
1084                    .flat_map(|l| &l.coedges)
1085                    .filter(|c| c.edge_id == cut.id)
1086                    .count();
1087                let exclusive_vertices = solid
1088                    .edges
1089                    .iter()
1090                    .filter(|e| {
1091                        e.start_vertex_id == cut.start_vertex_id
1092                            || e.end_vertex_id == cut.start_vertex_id
1093                            || e.start_vertex_id == cut.end_vertex_id
1094                            || e.end_vertex_id == cut.end_vertex_id
1095                    })
1096                    .all(|e| e.degenerate || cuts.contains(&e.id) || e.id == edge_id);
1097                let mut same_parameter = cut.t0 >= v0 && cut.t1 <= v1 && cut.t0 < cut.t1;
1098                for i in 0..=16 {
1099                    let t = cut.t0 + (cut.t1 - cut.t0) * i as f64 / 16.0;
1100                    let point = cut
1101                        .curve
1102                        .evaluate(t)
1103                        .or_refuse(KernelStage::Collect, "domain_cut_curve")?;
1104                    let carrier = face
1105                        .surface
1106                        .evaluate(u0, t)
1107                        .or_refuse(KernelStage::Collect, "domain_cut_carrier")?;
1108                    same_parameter &= point.sub(carrier).length() <= band;
1109                }
1110                domain_cut &= users == 2 && exclusive_vertices && same_parameter;
1111            }
1112            if !domain_cut {
1113                continue;
1114            }
1115            cut_ids.extend(cuts);
1116            additions.push(LoopRecord {
1117                id: take_id(),
1118                coedges: vec![rim.coedges.remove(position)],
1119            });
1120            separated = true;
1121        }
1122        if !additions.is_empty() {
1123            // The freed contact can cross the old parameter seam. Move the
1124            // artificial cut by a quarter turn, which is an exact shift of
1125            // this rational circle chart, to keep every real trim in-domain.
1126            let real = |id: u64| {
1127                !cut_ids.contains(&id) && !solid.edges.iter().any(|e| e.id == id && e.degenerate)
1128            };
1129            let curves: Vec<_> = face
1130                .loops
1131                .iter()
1132                .flat_map(|l| &l.coedges)
1133                .chain(additions.iter().flat_map(|l| &l.coedges))
1134                .filter(|c| real(c.edge_id) || c.edge_id == edge_id)
1135                .map(|c| &c.pcurve)
1136                .collect();
1137            let mut candidates = Vec::new();
1138            for shift in [0.25, 0.5, 0.75] {
1139                let mut clearance = f64::INFINITY;
1140                let mut fits = true;
1141                for curve in &curves {
1142                    let first = curve.control_points[0].x / curve.control_points[0].w;
1143                    let offset = -(first - shift).floor();
1144                    for p in &curve.control_points {
1145                        let u = p.x / p.w - shift + offset;
1146                        fits &= u >= 0.0 && u <= 1.0;
1147                        clearance = clearance.min(u.min(1.0 - u));
1148                    }
1149                }
1150                if fits {
1151                    candidates.push((clearance, shift));
1152                }
1153            }
1154            let Some((_, shift)) = candidates.into_iter().max_by(|a, b| a.0.total_cmp(&b.0)) else {
1155                return Err(KernelRefusal::unsupported(
1156                    KernelStage::Collect,
1157                    "sphere_domain_cut",
1158                    "blend: no quarter-turn sphere chart keeps the real trims in-domain",
1159                ));
1160            };
1161            let seam_direction = face
1162                .surface
1163                .evaluate(shift, 0.5)
1164                .or_refuse(KernelStage::Collect, "sphere_cut_direction")?
1165                .sub(atlas.centre);
1166            let surface = crate::make_sphere_surface_framed(
1167                atlas.centre,
1168                atlas.radius,
1169                atlas.basis[2],
1170                Some(seam_direction),
1171            )
1172            .or_refuse(KernelStage::Collect, "sphere_cut_surface")?;
1173            // Rebuilding must be only a chart change, including handedness.
1174            for i in 0..=16 {
1175                for v in [0.0, 0.3, 0.5, 0.7, 1.0] {
1176                    let u = i as f64 / 16.0;
1177                    let old = face
1178                        .surface
1179                        .evaluate(u, v)
1180                        .or_refuse(KernelStage::Collect, "sphere_cut_old")?;
1181                    let new = surface
1182                        .evaluate((u - shift).rem_euclid(1.0), v)
1183                        .or_refuse(KernelStage::Collect, "sphere_cut_new")?;
1184                    if old.sub(new).length() > band {
1185                        return Err(KernelRefusal::unsupported(
1186                            KernelStage::Collect,
1187                            "sphere_cut_chart",
1188                            "blend: the sphere quarter turn is not the same carrier",
1189                        ));
1190                    }
1191                }
1192            }
1193            let seam = surface
1194                .iso_curve_u(0.0)
1195                .or_refuse(KernelStage::Collect, "sphere_cut_seam")?;
1196            for coedge in face
1197                .loops
1198                .iter_mut()
1199                .flat_map(|l| &mut l.coedges)
1200                .chain(additions.iter_mut().flat_map(|l| &mut l.coedges))
1201            {
1202                if solid
1203                    .edges
1204                    .iter()
1205                    .any(|e| e.id == coedge.edge_id && e.degenerate)
1206                {
1207                    continue;
1208                }
1209                if !cut_ids.contains(&coedge.edge_id) {
1210                    let first =
1211                        coedge.pcurve.control_points[0].x / coedge.pcurve.control_points[0].w;
1212                    let offset = -(first - shift).floor() - shift;
1213                    for p in &mut coedge.pcurve.control_points {
1214                        p.x += offset * p.w;
1215                    }
1216                }
1217            }
1218            for cut in result.edges.iter_mut().filter(|e| cut_ids.contains(&e.id)) {
1219                for (vertex_id, t) in [(cut.start_vertex_id, cut.t0), (cut.end_vertex_id, cut.t1)] {
1220                    let point = seam
1221                        .evaluate(t)
1222                        .or_refuse(KernelStage::Collect, "sphere_cut_vertex")?;
1223                    let vertex = result
1224                        .vertices
1225                        .iter_mut()
1226                        .find(|v| v.id == vertex_id)
1227                        .ok_or(KernelRefusal::internal(
1228                            KernelStage::Collect,
1229                            "sphere_cut_vertex",
1230                            "blend: sphere seam vertex is missing",
1231                        ))?;
1232                    vertex.point = point;
1233                }
1234                cut.curve = seam.clone();
1235            }
1236            face.surface = surface;
1237        }
1238        face.loops.extend(additions);
1239    }
1240    if !separated {
1241        return Ok(None);
1242    }
1243    let old_vertex = solid
1244        .vertices
1245        .iter()
1246        .find(|v| v.id == edge.start_vertex_id)
1247        .ok_or(KernelRefusal::internal(
1248            KernelStage::Collect,
1249            "domain_cut_vertex",
1250            "blend: closed ring vertex is missing",
1251        ))?;
1252    let vertex_id = take_id();
1253    result.vertices.push(VertexRecord {
1254        id: vertex_id,
1255        point: old_vertex.point,
1256    });
1257    let ring = result
1258        .edges
1259        .iter_mut()
1260        .find(|e| e.id == edge_id)
1261        .ok_or(KernelRefusal::internal(
1262            KernelStage::Collect,
1263            "domain_cut_ring",
1264            "blend: closed ring edge is missing",
1265        ))?;
1266    ring.start_vertex_id = vertex_id;
1267    ring.end_vertex_id = vertex_id;
1268    Ok(Some(result))
1269}
1270
1271/// Imported faces can carry a slit from a closed rim to a pole and straight
1272/// back along the same UV track. It has no area and is not a periodic seam
1273/// (whose two pcurves differ by a period). Moving the rim must not drag this
1274/// artificial cut onto a different meridian. Cancel only adjacent opposite
1275/// uses of an edge with no other users, and prove that their UV tracks agree.
1276fn remove_retraced_cuts(
1277    solid: &BrepSolid,
1278    edge_id: u64,
1279) -> Result<Option<BrepSolid>, KernelRefusal> {
1280    let mut result = solid.clone();
1281    let mut removed = Vec::new();
1282    for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
1283        for rim in &mut face.loops {
1284            if !rim.coedges.iter().any(|coedge| coedge.edge_id == edge_id) {
1285                continue;
1286            }
1287            loop {
1288                let n = rim.coedges.len();
1289                let mut pair = None;
1290                for i in 0..n {
1291                    let j = (i + 1) % n;
1292                    let (a, b) = (&rim.coedges[i], &rim.coedges[j]);
1293                    if a.edge_id == edge_id || a.edge_id != b.edge_id || a.forward == b.forward {
1294                        continue;
1295                    }
1296                    let users = solid
1297                        .shells
1298                        .iter()
1299                        .flat_map(|s| &s.faces)
1300                        .flat_map(|f| &f.loops)
1301                        .flat_map(|l| &l.coedges)
1302                        .filter(|c| c.edge_id == a.edge_id)
1303                        .count();
1304                    if users != 2 {
1305                        continue;
1306                    }
1307                    let [a0, a1] = a
1308                        .pcurve
1309                        .domain()
1310                        .or_refuse(KernelStage::Collect, "cut_domain")?;
1311                    let [b0, b1] = b
1312                        .pcurve
1313                        .domain()
1314                        .or_refuse(KernelStage::Collect, "cut_domain")?;
1315                    // Compare the complete reversed rational representation,
1316                    // not a few probes that could miss a curved excursion.
1317                    let agrees =
1318                        a.pcurve.degree == b.pcurve.degree
1319                            && a.pcurve.knots.len() == b.pcurve.knots.len()
1320                            && a.pcurve.control_points.len() == b.pcurve.control_points.len()
1321                            && a.pcurve.knots.iter().zip(b.pcurve.knots.iter().rev()).all(
1322                                |(x, y)| {
1323                                    ((x - a0) / (a1 - a0) - (b1 - y) / (b1 - b0)).abs() <= 1e-9
1324                                },
1325                            )
1326                            && a.pcurve
1327                                .control_points
1328                                .iter()
1329                                .zip(b.pcurve.control_points.iter().rev())
1330                                .all(|(p, q)| {
1331                                    p.point()
1332                                        .ok()
1333                                        .zip(q.point().ok())
1334                                        .is_some_and(|(p, q)| p.sub(q).length() <= 1e-9)
1335                                        && (p.w / a.pcurve.control_points[0].w
1336                                            - q.w / b.pcurve.control_points.last().unwrap().w)
1337                                            .abs()
1338                                            <= 1e-9
1339                                });
1340                    if agrees {
1341                        pair = Some((i, j, a.edge_id));
1342                        break;
1343                    }
1344                }
1345                let Some((i, j, id)) = pair else {
1346                    break;
1347                };
1348                rim.coedges.remove(i.max(j));
1349                rim.coedges.remove(i.min(j));
1350                removed.push(id);
1351            }
1352        }
1353    }
1354    if removed.is_empty() {
1355        return Ok(None);
1356    }
1357    result.edges.retain(|edge| !removed.contains(&edge.id));
1358    super::open::prune_orphan_vertices(&mut result);
1359    Ok(Some(result))
1360}
1361
1362/// Where the second support crosses ITS seam-structured carrier's seam
1363/// meridian (both-seam closed edges).
1364struct SecondSeamSplit {
1365    fit_parameter: f64,
1366    crossing_v: f64,
1367    /// Signed carrier period travelled by the unwrapped pcurve track.
1368    period: f64,
1369}
1370
1371/// Replace the blended edge in both mating loops, trim the seam edge of an
1372/// anchored closed carrier, and insert the blend face.
1373/// The carve of a folding closed-edge wall: measure the fitted rows against
1374/// the carriers between the stations they were fitted through (the network's
1375/// own rail bar, half `intersection_fit`), trace the crease on the fitted
1376/// surface and fit it to `pcurve_consistency`, and refuse by name a lens
1377/// that straddles the wall's own seam (the closed rows join at u = 0 ≡ 1, and
1378/// the crease tracer reads one parameter rectangle).
1379/// Top of the closed march's carve ladder: `SEED_INTERVALS << 8` = 4096
1380/// intervals, the station count the chain lane's ladder also tops out near
1381/// (`CHAIN_CARVE_MAX_PER_SEGMENT` per segment).
1382const CARVE_MAX_REFINEMENT_DEPTH: usize = MAX_REFINEMENT_DEPTH + 4;
1383
1384/// How far the closed rows' rails stand off their carriers BETWEEN the
1385/// stations they were fitted through, at 0.25/0.5/0.75 of every span — the
1386/// open march's `rails_off_carriers` reading on closed rows.
1387fn rails_off_closed_carriers(
1388    rows: &FittedRows,
1389    stations: &[Station],
1390    parameters: &[f64],
1391    first: &BlendMate,
1392    second: &BlendMate,
1393) -> Result<f64, KernelRefusal> {
1394    Ok(closed_rail_interval_misses(rows, stations, parameters,
1395        [&first.face.surface, &second.face.surface])?.into_iter().fold(0.0_f64, f64::max))
1396}
1397
1398fn carve_closed_wall(
1399    solid: &BrepSolid,
1400    rows: &FittedRows,
1401    stations: &[Station],
1402    parameters: &[f64],
1403    first: &BlendMate,
1404    second: &BlendMate,
1405    radius: f64,
1406) -> Result<crate::blend::carve::CarvedCrease, KernelRefusal> {
1407    let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
1408    let rail_bar = 0.5 * policy.intersection_fit;
1409    let off = rails_off_closed_carriers(rows, stations, parameters, first, second)?;
1410    crate::blend::carve::carve_trace(format_args!(
1411        "blend closed-edge carve: {} stations, rails {off:.3e} off their carriers between stations (bar {rail_bar:.3e})",
1412        parameters.len()
1413    ));
1414    if off > rail_bar {
1415        return Err(crate::blend::fold::wall_fold_refusal(
1416            Vec::new(),
1417            format!(
1418                "{} {radius} fits this edge: the wall folds, and the closed march's fitted rails stand {off:.3e} off their carriers between stations (bar {rail_bar:.3e}), so there is no wall accurate enough to carve",
1419                crate::blend::WALL_FOLDS
1420            ),
1421        ));
1422    }
1423    let Some(carved) = crate::blend::carve::trace_wall_crease(&rows.surface, radius)? else {
1424        return Err(KernelRefusal::internal(KernelStage::Refine, "carve_no_fold", "blend closed-edge carve: the wall re-marched under Carve carries no fold to carve, but the march refused one"));
1425    };
1426    let [u0, u1] = rows.u_domain;
1427    let margin = 1e-3 * (u1 - u0);
1428    let straddles = carved.ends.iter().any(|end| end[0] <= u0 + margin || end[0] >= u1 - margin);
1429    if straddles {
1430        return Err(crate::blend::fold::wall_fold_refusal(
1431            Vec::new(),
1432            format!(
1433                "{} {radius} fits this edge: the wall folds, and the fold band straddles the wall's own seam (lens ends at u = {:.6} and {:.6} of [{u0:.3}, {u1:.3}]), which the crease tracer reads as two half-lenses; carving across the seam is not built",
1434                crate::blend::WALL_FOLDS, carved.ends[0][0], carved.ends[1][0]
1435            ),
1436        ));
1437    }
1438    crate::blend::carve::fit_crease(&rows.surface, &carved, policy.pcurve_consistency)
1439}
1440
1441/// Lane G row 2 (2026-10-14, `closed_trim_read_first_on`): a closed rail's
1442/// support trim on a PLANE mate is the rail's own control points carried
1443/// through the plane's affine chart (an exact image of the rail, the exact
1444/// revolution lane's rule for a plane mate's rail, `plane_affine_pcurve`),
1445/// read once as shipped at the pcurve floor (`track_fit::read_shipped_pcurve`,
1446/// the read the refit itself is judged on, at the rail's own parameter) and
1447/// kept when it reads within it (8.98e-15 on the half torus's cut plane);
1448/// any other mate, or an image that misses, takes the refit as before. The
1449/// shipped contract (every trim read within the floor) is the refit's own;
1450/// what changes is the bytes of a plane trim (the refit's control points are
1451/// the track fitter's, these are the rail's own), which is why the road is
1452/// behind its switch until its census. The march's own interpolated pcurve
1453/// was measured as a second candidate and dropped: on the half torus it
1454/// reads 8.5e-5 off its rail where the uv track crosses the carrier's knot
1455/// lines (the chart is C1 there, a cubic interpolant through the stations is
1456/// not), the two reads cost 2.8 ms for nothing, and a rational carrier's
1457/// knot lines make that the rule, not the exception.
1458fn closed_support_trim(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1459    let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1460    let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1461    let within_floor = |candidate: &NurbsCurve, what: &str| -> bool {
1462        let _read_timing = crate::blend::BlendTimer::start("timing.closed.rows.trim_read_ms");
1463        let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), candidate, d0, d1);
1464        if std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some() {
1465            match &read {
1466                Ok((miss, standoff, samples, worst)) => eprintln!(
1467                    "TRACK_FIT closed-edge support, {what} candidate ({} control points): miss {miss:.3e} at t {worst:.9} ({samples} samples), rail standoff {standoff:.3e}, floor {floor:e}",
1468                    candidate.control_points.len()),
1469                Err(error) => eprintln!("TRACK_FIT closed-edge support, {what} candidate: unreadable: {error}"),
1470            }
1471        }
1472        read.is_ok_and(|(miss, _, _, _)| miss <= floor)
1473    };
1474    if let Some(affine) = plane_affine_pcurve(rail, surface) {
1475        if within_floor(&affine, "affine plane image") {
1476            crate::blend::record_count("count.closed.rows.trim.affine", 1);
1477            return Ok(affine);
1478        }
1479    }
1480    crate::blend::record_count("count.closed.rows.trim.refit", 1);
1481    refit_closed_support_pcurve(rail, old, surface)
1482}
1483
1484/// The rail's own control points through a PLANE mate's affine chart (the
1485/// chart read off the surface's domain corners, as the exact revolution
1486/// lane's `plane_pcurve` reads it), on the rail's knots and weights: the
1487/// exact image of the rail in (u, v). `None` on any other carrier, or on a
1488/// chart too thin to invert.
1489fn plane_affine_pcurve(rail: &NurbsCurve, surface: &NurbsSurface) -> Option<NurbsCurve> {
1490    if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) {
1491        return None;
1492    }
1493    let [u0, u1] = surface.domain_u().ok()?;
1494    let [v0, v1] = surface.domain_v().ok()?;
1495    let origin = surface.evaluate(u0, v0).ok()?;
1496    let du = surface.evaluate(u1, v0).ok()?.sub(origin).scale(1.0 / (u1 - u0));
1497    let dv = surface.evaluate(u0, v1).ok()?.sub(origin).scale(1.0 / (v1 - v0));
1498    let (a, b, c) = (du.dot(du), du.dot(dv), dv.dot(dv));
1499    let det = a * c - b * b;
1500    let chart_scale = origin.length().max(du.length()).max(dv.length()).max(1.0);
1501    if !(det > 1e-24 * chart_scale.powi(4)) {
1502        return None;
1503    }
1504    let mut pcurve = rail.clone();
1505    for control in &mut pcurve.control_points {
1506        let weight = control.w;
1507        let offset = Vec3::new(control.x / weight, control.y / weight, control.z / weight).sub(origin);
1508        let (r, s) = (offset.dot(du), offset.dot(dv));
1509        control.x = (u0 + (c * r - b * s) / det) * weight;
1510        control.y = (v0 + (a * s - b * r) / det) * weight;
1511        control.z = 0.0;
1512    }
1513    Some(pcurve)
1514}
1515
1516/// A support rail's pcurve on its mate `surface` (a closed edge's whole rail,
1517/// or an open edge's rail trimmed to its crossing window), fitted to the
1518/// rail's own projected track at the pcurve floor
1519/// (`chain::project_piece_pcurve`, the closed chain's support-piece fitter,
1520/// which refuses a fit off that floor), on the rail's parameter domain and in
1521/// the same carrier period as the interpolated pcurve `old` it replaces. A
1522/// closed rail around a periodic carrier spans a whole period, so its ends are
1523/// not pinned by the fitter (both would go to one seam); instead an end whose
1524/// `old` coordinate lies exactly on a seam keeps that coordinate, so the loop
1525/// still closes on the seam the surgery built it on.
1526///
1527/// The shipped curve is read again at the floor (`read_shipped_pcurve`). The
1528/// fitter's stencil reads j/16 of each TRACK interval and the shipped read
1529/// j/16 of each span of the fitted curve's own (averaged) knots, so a fit the
1530/// stencil reads just under the floor can read just over it as shipped (the
1531/// 2026-09-27 revolve: 9.923e-8 on the stencil, 1.037e-7 at t 0.4902). Such
1532/// a fit is refitted, at the SAME floor, with its track refined where the
1533/// shipped read found the miss, up to `SHIPPED_READ_REPAIRS` times; the floor
1534/// never moves, and a fit read within it the first time ships as before.
1535///
1536/// Its first fit, whose floor miss is TERMINAL on this road, takes the
1537/// knot-aware retry (lane L, 2026-10-16); the open surgery's own refit
1538/// (`refit_closed_support_pcurve_read`), which answers a miss with its
1539/// fallback, does not.
1540pub(in crate::blend) fn refit_closed_support_pcurve(rail: &NurbsCurve, old: &NurbsCurve, surface: &NurbsSurface) -> Result<NurbsCurve, KernelRefusal> {
1541    let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1542    refit_closed_support_pcurve_read_with(rail, old, surface, &mut crate::blend::track_fit::FootMemo::transient(), knot_retry).map(|(fitted, _)| fitted)
1543}
1544
1545thread_local! {
1546    /// Set only while [`blend_closed_edge_knot_aware`] or
1547    /// [`with_pcurve_knot_retry`] re-runs a blend whose first run was refused
1548    /// for a support pcurve off the floor (lane L, 2026-10-16).
1549    static KNOT_RETRY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1550}
1551
1552/// [`refit_closed_support_pcurve`] handing back the shipped curve's own read
1553/// `(miss, rail standoff, samples, worst parameter)` -- the
1554/// `read_shipped_pcurve` of the curve returned, which a caller that reads
1555/// the shipped trim again would otherwise compute a second time on the same
1556/// inputs -- with the feet of that read left in `memo` (cleared before each
1557/// fit, so it holds the returned curve's feet only) for the caller's
1558/// further reads of the same curve against the same rail.
1559pub(in crate::blend) fn refit_closed_support_pcurve_read(
1560    rail: &NurbsCurve,
1561    old: &NurbsCurve,
1562    surface: &NurbsSurface,
1563    memo: &mut crate::blend::track_fit::FootMemo,
1564) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1565    // The open surgery's refit takes the knot-aware retry only inside
1566    // `with_pcurve_knot_retry` (lane L, 2026-10-16, lCount_229).
1567    let knot_retry = KNOT_RETRY.with(|flag| flag.get());
1568    refit_closed_support_pcurve_read_with(rail, old, surface, memo, knot_retry)
1569}
1570
1571/// Runs `build` with every support refit taking the knot-aware retry on a
1572/// floor miss, restoring the flag after (lane L, 2026-10-16, lCount_229: the
1573/// fillet group's network re-run on a pcurve-floor refusal).
1574pub fn with_pcurve_knot_retry<T>(build: impl FnOnce() -> T) -> T {
1575    let previous = KNOT_RETRY.with(|flag| flag.replace(true));
1576    let built = build();
1577    KNOT_RETRY.with(|flag| flag.set(previous));
1578    built
1579}
1580
1581/// [`refit_closed_support_pcurve_read`], its first fit taking the knot-aware
1582/// retry when `knot_retry` (see [`refit_closed_support_pcurve`]).
1583fn refit_closed_support_pcurve_read_with(
1584    rail: &NurbsCurve,
1585    old: &NurbsCurve,
1586    surface: &NurbsSurface,
1587    memo: &mut crate::blend::track_fit::FootMemo,
1588    knot_retry: bool,
1589) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1590    const SHIPPED_READ_REPAIRS: usize = 3;
1591    let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1592    let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1593    let mut near: Vec<f64> = Vec::new();
1594    // The FIRST fit's floor miss is terminal here, so it takes the knot-aware
1595    // retry (lane L, 2026-10-16, lCount_201); a repair round's miss keeps the
1596    // last fit, as before.
1597    let (mut fitted, mut read) = fit_and_read_support(rail, old, surface, &near, memo, knot_retry)?;
1598    for _ in 0..SHIPPED_READ_REPAIRS {
1599        if read.0 <= floor || !(d1 > d0) {
1600            break;
1601        }
1602        near.push((read.3 - d0) / (d1 - d0));
1603        crate::blend::record_count("count.surgery.refit_repairs", 1);
1604        // A repair that fails keeps the last fit AND its feet: a failed
1605        // fit or an unreadable read hands nothing to the memo.
1606        match fit_and_read_support(rail, old, surface, &near, memo, false) {
1607            Ok(repaired) => (fitted, read) = repaired,
1608            Err(_) => break,
1609        }
1610    }
1611    let (miss, standoff, samples, _) = read;
1612    if !(miss <= floor) {
1613        return Err(KernelRefusal::non_convergence(KernelStage::Refine, CLOSED_SUPPORT_PCURVE_FLOOR_WHAT, format!(
1614            "{} a blend's support pcurve, as shipped, misses its rail's track by {miss:.3e} at {samples} samples, \
1615             against a floor of {floor:.1e}",
1616            crate::blend::PCURVE_OFF_FLOOR,
1617        )));
1618    }
1619    Ok((fitted, read))
1620}
1621
1622/// [`refit_closed_support_pcurve`]'s fit, its track refined near the
1623/// fractions `near` first, and the shipped curve's read (miss, standoff,
1624/// samples, worst rail parameter).
1625fn fit_and_read_support(
1626    rail: &NurbsCurve,
1627    old: &NurbsCurve,
1628    surface: &NurbsSurface,
1629    near: &[f64],
1630    memo: &mut crate::blend::track_fit::FootMemo,
1631    knot_retry: bool,
1632) -> Result<(NurbsCurve, (f64, f64, usize, f64)), KernelRefusal> {
1633    let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1634    // Every foot of the track is sought from the march's own trim `old` at
1635    // the same rail parameter (`pcurve_guide`), on the branch it runs on.
1636    let guide = crate::blend::chain::pcurve_guide(old, surface)?;
1637    let track_timing = crate::blend::BlendTimer::start("timing.support_refit.track_ms");
1638    let mut fitted = if knot_retry {
1639        crate::blend::chain::project_piece_pcurve_within_knot_retry(rail, surface, false, false, Some(&guide), near)?
1640    } else {
1641        crate::blend::chain::project_piece_pcurve_within(rail, surface, false, false, Some(&guide), near)?
1642    };
1643    track_timing.stop();
1644    // The fitter's parameter is the track fraction; the surgery reads the
1645    // pcurve at the rail's own parameter.
1646    let [f0, f1] = fitted.domain().or_refuse(KernelStage::Refine, "domain")?;
1647    for knot in fitted.knots.iter_mut() {
1648        *knot = d0 + (*knot - f0) / (f1 - f0) * (d1 - d0);
1649    }
1650    let debug = std::env::var_os("BREP_DEBUG_TRACK_FIT").is_some();
1651    if debug {
1652        // Diagnostic only: the same read before the period shift and the seam
1653        // ends are set (the shift alone moves no point of the carrier).
1654        match crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &fitted, d0, d1) {
1655            Ok((miss, standoff, samples, worst)) => eprintln!(
1656                "TRACK_FIT closed-edge support: before the seam ends, miss {miss:.3e} at t {worst:.9} ({samples} samples), standoff {standoff:.3e}"),
1657            Err(error) => eprintln!("TRACK_FIT closed-edge support: before the seam ends, unreadable: {error}"),
1658        }
1659    }
1660    let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
1661    let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1662    let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1663    let old_start = old.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1664    let old_end = old.evaluate(d1).or_refuse(KernelStage::Refine, "evaluate")?;
1665    let new_start = fitted.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?;
1666    // Whole periods only: the same carrier point, in the old pcurve's period.
1667    let shift = |closed: bool, from: f64, to: f64, period: f64| if closed { ((to - from) / period).round() * period } else { 0.0 };
1668    let (shift_u, shift_v) = (shift(closed_u, new_start.x, old_start.x, u1 - u0), shift(closed_v, new_start.y, old_start.y, v1 - v0));
1669    for point in fitted.control_points.iter_mut() {
1670        point.x += shift_u * point.w;
1671        point.y += shift_v * point.w;
1672    }
1673    let on_seam = |closed: bool, value: f64, low: f64, high: f64| {
1674        let period = high - low;
1675        closed && ((value - low) / period - ((value - low) / period).round()).abs() * period <= 1e-12 * period.max(1.0)
1676    };
1677    let last = fitted.control_points.len() - 1;
1678    for (index, end) in [(0, old_start), (last, old_end)] {
1679        let point = &mut fitted.control_points[index];
1680        if on_seam(closed_u, end.x, u0, u1) {
1681            point.x = end.x * point.w;
1682        }
1683        if on_seam(closed_v, end.y, v0, v1) {
1684            point.y = end.y * point.w;
1685        }
1686    }
1687    // The curve SHIPPED is not the one the fitter checked (its domain, period
1688    // and seam ends were edited), so it is read again, at the rail's own
1689    // parameter, against the fitter's floor; a miss refuses as the fitter's
1690    // own would, and an unreadable sample refuses.
1691    // The feet are the shipped curve's, read into a memo of their own and
1692    // handed over only with a readable curve: a read that refuses (an
1693    // unreadable sample, found after every sample was sought) must leave
1694    // the caller's memo holding the feet of the curve the caller keeps,
1695    // not this curve's.
1696    let mut own = memo.empty_like();
1697    let (miss, standoff, samples, worst) =
1698        crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut own)?;
1699    *memo = own;
1700    if debug {
1701        eprintln!("TRACK_FIT closed-edge support: shipped pcurve miss {miss:.3e} at t {worst:.9} of [{d0:.9}, {d1:.9}] ({samples} samples); rail standoff from its carrier {standoff:.3e}");
1702    }
1703    Ok((fitted, (miss, standoff, samples, worst)))
1704}
1705
1706
1707/// One exit probe: whether a support station's `uv` (wrapped into a closed
1708/// carrier's domain) classifies OUTSIDE `face`'s trims, an unreadable class
1709/// counting as outside.
1710fn support_exits_at(face: &FaceRecord, mut uv: [f64; 2]) -> bool {
1711    if let Ok((closed_u, closed_v)) = face.surface.closed_directions() {
1712        if closed_u {
1713            if let Ok([low, high]) = face.surface.domain_u() {
1714                uv[0] = low + (uv[0] - low).rem_euclid(high - low);
1715            }
1716        }
1717        if closed_v {
1718            if let Ok([low, high]) = face.surface.domain_v() {
1719                uv[1] = low + (uv[1] - low).rem_euclid(high - low);
1720            }
1721        }
1722    }
1723    crate::parameter_point_in_face(face, crate::Vec2 { x: uv[0], y: uv[1] }, 1e-6)
1724        .map(|class| class == crate::PolygonClass::Outside)
1725        .unwrap_or(true)
1726}
1727
1728/// The exit question for a mate bounded only by PARALLELS of the rim's axis
1729/// (lane R 2026-10-28, the cylinder rims row: 17 probes per mate were 0.15 ms
1730/// of a two-rim fillet's ~1.5 ms).
1731///
1732/// A plane normal to the axis or a cylinder/cone about it is cut by the
1733/// parallels into cells — annuli of the plane between consecutive radii,
1734/// bands of the cylinder between consecutive heights — and when every
1735/// boundary edge of the face (seams, used twice by one loop, aside) is such a
1736/// parallel, each cell is wholly inside the face or wholly outside it.  The
1737/// support contacts of a rim between coaxial carriers are parallels too, so
1738/// when every station's meridian coordinate (radius on a plane, height on a
1739/// cylinder or cone) lies in ONE open cell, clear of its bounding parallels
1740/// by a margin far above the probe's 1e-6 band, every station classifies the
1741/// same, and the first station's probe is the scan's answer.  `None` — the
1742/// full scan — for any other carrier, any boundary that is not a parallel,
1743/// and any station within the margin of a boundary, so a track that does
1744/// meet a boundary is classified station by station exactly as before.
1745fn support_exit_between_parallels(
1746    solid: &BrepSolid,
1747    face: &FaceRecord,
1748    (origin, axis): (Vec3, Vec3),
1749    contacts: &[Vec3],
1750    probe: &dyn Fn(usize) -> bool,
1751) -> Option<bool> {
1752    let radial = match face.surface.analytic()? {
1753        crate::AnalyticSurface::Plane { .. } => true,
1754        crate::AnalyticSurface::RuledRevolution { .. } => false,
1755        _ => return None,
1756    };
1757    if contacts.is_empty() {
1758        return None;
1759    }
1760    let axis = axis.normalized().ok()?;
1761    let coordinates = |point: Vec3| -> (f64, f64) {
1762        let offset = point.sub(origin);
1763        let height = offset.dot(axis);
1764        (offset.sub(axis.scale(height)).length(), height)
1765    };
1766    let meridian = |point: Vec3| -> f64 {
1767        let (radius, height) = coordinates(point);
1768        if radial { radius } else { height }
1769    };
1770    let scale = contacts.iter().map(|point| point.sub(origin).length()).fold(1.0, f64::max);
1771    let parallel_band = 1e-9 * scale;
1772    let margin = 1e-5 * scale;
1773    // Every boundary edge but the seams must be a parallel: one radius and
1774    // one height along the whole edge, read at nine points.
1775    let mut boundaries: Vec<f64> = Vec::new();
1776    for loop_record in &face.loops {
1777        for coedge in &loop_record.coedges {
1778            if loop_record.coedges.iter().filter(|other| other.edge_id == coedge.edge_id).count() == 2 {
1779                continue;
1780            }
1781            let boundary = solid.edges.iter().find(|candidate| candidate.id == coedge.edge_id)?;
1782            if boundary.degenerate {
1783                continue;
1784            }
1785            let mut read: Option<(f64, f64)> = None;
1786            for step in 0..=8 {
1787                let point = boundary.curve.evaluate(boundary.t0 + (boundary.t1 - boundary.t0) * step as f64 / 8.0).ok()?;
1788                let here = coordinates(point);
1789                match read {
1790                    None => read = Some(here),
1791                    Some(first) => {
1792                        if (here.0 - first.0).abs() > parallel_band || (here.1 - first.1).abs() > parallel_band {
1793                            return None;
1794                        }
1795                    }
1796                }
1797            }
1798            let (radius, height) = read?;
1799            boundaries.push(if radial { radius } else { height });
1800        }
1801    }
1802    // One open cell holds every station, clear of every boundary parallel.
1803    let readings: Vec<f64> = contacts.iter().map(|point| meridian(*point)).collect();
1804    let low = readings.iter().copied().fold(f64::INFINITY, f64::min);
1805    let high = readings.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1806    if !(low.is_finite() && high.is_finite()) {
1807        return None;
1808    }
1809    if boundaries.iter().any(|boundary| *boundary >= low - margin && *boundary <= high + margin) {
1810        return None;
1811    }
1812    Some(probe(0))
1813}
1814
1815fn build_surgery(
1816    solid: &BrepSolid,
1817    edge: &EdgeRecord,
1818    first: &BlendMate,
1819    second: &BlendMate,
1820    rows: FittedRows,
1821    second_split: Option<SecondSeamSplit>,
1822    name: Option<&str>,
1823    crease: Option<crate::blend::carve::CarvedCrease>,
1824) -> Result<BrepSolid, KernelRefusal> {
1825    let mut result = solid.clone();
1826    let mut take_id = crate::blend::edge::fresh_id_source(solid);
1827
1828    let [u_start, u_end] = rows.u_domain;
1829    let cr_start = rows.cr.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1830    let cs_start = rows.cs.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1831    let vertex1_id = take_id();
1832    let vertex2_id = take_id();
1833    result.vertices.push(VertexRecord {
1834        id: vertex1_id,
1835        point: cr_start,
1836    });
1837    result.vertices.push(VertexRecord {
1838        id: vertex2_id,
1839        point: cs_start,
1840    });
1841
1842    let cr_edge_id = take_id();
1843    result.edges.push(EdgeRecord {
1844        id: cr_edge_id,
1845        curve: rows.cr.clone(),
1846        t0: u_start,
1847        t1: u_end,
1848        start_vertex_id: vertex1_id,
1849        end_vertex_id: vertex1_id,
1850        degenerate: false,
1851        name: None,
1852    });
1853    // Second support: whole closed edge, or two pieces split where it
1854    // crosses ITS carrier's seam meridian (both-seam closed edges).  Each
1855    // piece records (edge id, fit-space window, forward pcurve).
1856    let mut cs_pieces: Vec<(u64, [f64; 2], NurbsCurve)> = Vec::new();
1857    let mut second_seam_vertex = vertex2_id;
1858    let mut second_seam_point = cs_start;
1859    let mut second_crossing_v = 0.0;
1860    if let Some(split) = &second_split {
1861        let p = split.fit_parameter;
1862        second_crossing_v = split.crossing_v;
1863        let (cs_a, cs_b) = rows.cs.split(p).or_refuse(KernelStage::Refine, "split")?;
1864        let (pc_a, pc_b) = rows.cs_pcurve.split(p).or_refuse(KernelStage::Refine, "split")?;
1865        // Bring the wrapped second piece back into the carrier's domain.
1866        let mut pc_b = pc_b;
1867        for point in pc_b.control_points.iter_mut() {
1868            point.x -= split.period * point.w;
1869        }
1870        let w2s = take_id();
1871        second_seam_vertex = w2s;
1872        second_seam_point = rows.cs.evaluate(p).or_refuse(KernelStage::Refine, "evaluate")?;
1873        result.vertices.push(VertexRecord {
1874            id: w2s,
1875            point: second_seam_point,
1876        });
1877        let edge_a = take_id();
1878        result.edges.push(EdgeRecord {
1879            id: edge_a,
1880            curve: cs_a,
1881            t0: u_start,
1882            t1: p,
1883            start_vertex_id: vertex2_id,
1884            end_vertex_id: w2s,
1885            degenerate: false,
1886            name: None,
1887        });
1888        cs_pieces.push((edge_a, [u_start, p], pc_a));
1889        let edge_b = take_id();
1890        result.edges.push(EdgeRecord {
1891            id: edge_b,
1892            curve: cs_b,
1893            t0: p,
1894            t1: u_end,
1895            start_vertex_id: w2s,
1896            end_vertex_id: vertex2_id,
1897            degenerate: false,
1898            name: None,
1899        });
1900        cs_pieces.push((edge_b, [p, u_end], pc_b));
1901    } else {
1902        let cs_edge_id = take_id();
1903        result.edges.push(EdgeRecord {
1904            id: cs_edge_id,
1905            curve: rows.cs.clone(),
1906            t0: u_start,
1907            t1: u_end,
1908            start_vertex_id: vertex2_id,
1909            end_vertex_id: vertex2_id,
1910            degenerate: false,
1911            name: None,
1912        });
1913        cs_pieces.push((cs_edge_id, [u_start, u_end], rows.cs_pcurve.clone()));
1914    }
1915    let seam_curve = rows.surface.iso_curve_u(u_start).or_refuse(KernelStage::Refine, "iso_curve_u")?;
1916    let [seam_t0, seam_t1] = seam_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1917    let blend_seam_id = take_id();
1918    result.edges.push(EdgeRecord {
1919        id: blend_seam_id,
1920        curve: seam_curve,
1921        t0: seam_t0,
1922        t1: seam_t1,
1923        start_vertex_id: vertex1_id,
1924        end_vertex_id: vertex2_id,
1925        degenerate: false,
1926        name: None,
1927    });
1928
1929    let replace_in_face = |result: &mut BrepSolid, mate: &BlendMate, pieces: &[(u64, [f64; 2], NurbsCurve)], uv_new: [f64; 2]| -> Result<bool, KernelRefusal> {
1930        replace_rim_in_face(result, edge, mate, pieces, uv_new)
1931    };
1932    let uv1_new = rows.cr_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1933    let v1_new = uv1_new.y;
1934    let cr_pieces = vec![(cr_edge_id, [u_start, u_end], rows.cr_pcurve.clone())];
1935    SEAM_CONSUMED.with(|flag| flag.set(None));
1936    let mut repaired = replace_in_face(&mut result, first, &cr_pieces, [uv1_new.x, v1_new])?;
1937    let uv2_new = rows.cs_pcurve.evaluate(u_start).or_refuse(KernelStage::Refine, "evaluate")?;
1938    let v2_new = if second_split.is_some() {
1939        second_crossing_v
1940    } else {
1941        uv2_new.y
1942    };
1943    repaired |= replace_in_face(&mut result, second, &cs_pieces, [uv2_new.x, v2_new])?;
1944
1945    // A contact circle about an axis oblique to a sphere's stored polar axis
1946    // can wind once around that sphere's U period.  Such a circle is not an
1947    // ordinary hole in the full-domain sphere rectangle: it bounds a cap with
1948    // one of the collapsed pole rims.  Keep that pole as the companion loop so
1949    // containment, integration, and tessellation see the actual cap topology.
1950    let collapse_winding_sphere_cap =
1951        |result: &mut BrepSolid, face_id: u64, support_ids: &[u64]| -> Result<(), KernelRefusal> {
1952            let face = result
1953                .shells
1954                .iter_mut()
1955                .flat_map(|shell| &mut shell.faces)
1956                .find(|face| face.id == face_id)
1957                .ok_or(KernelRefusal::internal(KernelStage::Sew, "sphere_carrier", "blend: sphere carrier lost during cap surgery"))?;
1958            if !matches!(
1959                face.surface.analytic(),
1960                Some(crate::AnalyticSurface::Sphere { .. })
1961            ) || face.surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")? != (true, false)
1962            {
1963                return Ok(());
1964            }
1965            let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
1966            let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
1967            let period = u1 - u0;
1968            let support_loop = face.loops.iter().position(|loop_record| {
1969                !loop_record.coedges.is_empty()
1970                    && loop_record
1971                        .coedges
1972                        .iter()
1973                        .all(|coedge| support_ids.contains(&coedge.edge_id))
1974            });
1975            let Some(support_loop) = support_loop else {
1976                return Ok(());
1977            };
1978            if face.loops[support_loop].coedges.len() != 1 {
1979                return Ok(());
1980            }
1981            let support = &face.loops[support_loop].coedges[0];
1982            let [s0, s1] = support.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1983            let support_start = support.pcurve.evaluate(s0).or_refuse(KernelStage::Refine, "evaluate")?;
1984            let support_end = support.pcurve.evaluate(s1).or_refuse(KernelStage::Refine, "evaluate")?;
1985            let support_winding = support_end.x - support_start.x;
1986            if (support_winding.abs() - period).abs() > 0.05 * period {
1987                return Ok(());
1988            }
1989
1990            let mut pole: Option<(usize, CoedgeRecord)> = None;
1991            for (loop_index, loop_record) in face.loops.iter().enumerate() {
1992                if loop_index == support_loop {
1993                    continue;
1994                }
1995                for coedge in &loop_record.coedges {
1996                    let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
1997                    let start = coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?;
1998                    let end = coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?;
1999                    let winding = end.x - start.x;
2000                    let at_pole = (start.y - v0).abs() <= 1e-8 || (start.y - v1).abs() <= 1e-8;
2001                    if at_pole
2002                        && (end.y - start.y).abs() <= 1e-8
2003                        && (winding.abs() - period).abs() <= 0.05 * period
2004                        && winding * support_winding < 0.0
2005                    {
2006                        pole = Some((loop_index, coedge.clone()));
2007                        break;
2008                    }
2009                }
2010                if pole.is_some() {
2011                    break;
2012                }
2013            }
2014            let Some((pole_loop, pole_coedge)) = pole else {
2015                return Ok(());
2016            };
2017            let support_record = face.loops[support_loop].clone();
2018            let pole_id = face.loops[pole_loop].id;
2019            face.loops = vec![
2020                LoopRecord {
2021                    id: pole_id,
2022                    coedges: vec![pole_coedge],
2023                },
2024                support_record,
2025            ];
2026            Ok(())
2027        };
2028    collapse_winding_sphere_cap(&mut result, first.face.id, &[cr_edge_id])?;
2029    let cs_edge_ids: Vec<u64> = cs_pieces.iter().map(|(edge_id, ..)| *edge_id).collect();
2030    collapse_winding_sphere_cap(&mut result, second.face.id, &cs_edge_ids)?;
2031    // Patch the zero coedge ids introduced for extra pieces.
2032    for shell in &mut result.shells {
2033        for face in &mut shell.faces {
2034            for loop_record in &mut face.loops {
2035                for coedge in &mut loop_record.coedges {
2036                    if coedge.id == 0 {
2037                        coedge.id = take_id();
2038                    }
2039                }
2040            }
2041        }
2042    }
2043
2044    // Trim seam EDGES that ended on the removed vertex: their endpoint
2045    // moves onto the support-curve crossing on THEIR carrier (first
2046    // mate's seam -> the blend seam vertex; second mate's -> its own
2047    // seam-crossing vertex).
2048    let new_edge_ids: Vec<u64> = std::iter::once(cr_edge_id)
2049        .chain(cs_pieces.iter().map(|(edge_id, ..)| *edge_id))
2050        .chain(std::iter::once(blend_seam_id))
2051        .collect();
2052    let first_face_edges: Vec<u64> = first
2053        .face
2054        .loops
2055        .iter()
2056        .flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
2057        .collect();
2058    repaired |= trim_seam_edges(
2059        &mut result,
2060        edge,
2061        &new_edge_ids,
2062        &first_face_edges,
2063        (vertex1_id, v1_new, cr_start),
2064        (second_seam_vertex, v2_new, second_seam_point),
2065    )?;
2066
2067    // Blend face: outward orientation matches F1's outward at the v=0 rim.
2068    let mid_u = (u_start + u_end) * 0.5;
2069    let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2070    let station_uv = rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
2071    let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2072    let out1 = if first.face.same_sense {
2073        n1
2074    } else {
2075        n1.scale(-1.0)
2076    };
2077    // A rolling ball's wall is tangent to F1 at the rim, so F1's outward
2078    // normal settles its side.  An asymmetric chamfer's bevel is not: a steep
2079    // one (a short chord on F1, a long one on F2) turns more than a right
2080    // angle from F1, and F1's normal alone reads it backwards.  The bevel
2081    // always lies between its two faces, so it is read against both.
2082    let same_sense = if crate::blend::setback::active().is_some() {
2083        let uv2 = rows.cs_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?;
2084        let n2 = raw_normal(&second.face.surface, uv2.x, uv2.y)?;
2085        let out2 = if second.face.same_sense { n2 } else { n2.scale(-1.0) };
2086        let v_far = rows.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?[1];
2087        let far_normal = raw_normal(&rows.surface, mid_u, v_far)?;
2088        blend_normal.dot(out1) + far_normal.dot(out2) >= 0.0
2089    } else {
2090        blend_normal.dot(out1) >= 0.0
2091    };
2092    let loop_id = take_id();
2093    let mut coedges = vec![
2094        CoedgeRecord {
2095            id: take_id(),
2096            edge_id: cr_edge_id,
2097            forward: true,
2098            pcurve: crate::sweep_topology::parameter_line(u_start, 0.0, u_end, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2099        },
2100        CoedgeRecord {
2101            id: take_id(),
2102            edge_id: blend_seam_id,
2103            forward: true,
2104            pcurve: crate::sweep_topology::parameter_line(u_end, 0.0, u_end, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2105        },
2106    ];
2107    for (edge_id, window, _) in cs_pieces.iter().rev() {
2108        coedges.push(CoedgeRecord {
2109            id: take_id(),
2110            edge_id: *edge_id,
2111            forward: false,
2112            pcurve: crate::sweep_topology::parameter_line(window[1], 1.0, window[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2113        });
2114    }
2115    coedges.push(CoedgeRecord {
2116        id: take_id(),
2117        edge_id: blend_seam_id,
2118        forward: false,
2119        pcurve: crate::sweep_topology::parameter_line(u_start, 1.0, u_start, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?,
2120    });
2121    // The coedges above trace the parameter rectangle counter-clockwise in
2122    // (u, v); that traversal is only the outward boundary when the blend
2123    // surface normal already points outward (same_sense).  When it does not
2124    // (a mate whose support rim runs the other way — e.g. the reversed cap
2125    // of a seam carrier), the loop must wind the OTHER way so its coedges
2126    // traverse the shared support edges opposite to the mate, and so the
2127    // classification tangent test (parameter_point_in_face) stays consistent
2128    // with same_sense.  Reversing the coedge order and each coedge (forward
2129    // flag + pcurve) flips the winding without touching same_sense.
2130    if !same_sense {
2131        coedges.reverse();
2132        for coedge in &mut coedges {
2133            coedge.forward = !coedge.forward;
2134            coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
2135        }
2136    }
2137    let mut loops = vec![LoopRecord {
2138        id: loop_id,
2139        coedges,
2140    }];
2141    // The CARVED lens: an INNER loop of two coedges on ONE new edge — the
2142    // crease — both of them this face's own, the slit pattern the closed
2143    // chain lane sews (`chain/closed_surgery.rs`): the two sheets of the wall
2144    // are one surface, so the curve where they meet has one edge and two
2145    // coedges.  The lens between them is the part of the swept envelope
2146    // inside the swept ball volume, and dropping it is what makes the wall
2147    // the blend.  Winding: the inner loop runs AGAINST the outer one, read
2148    // off both loops' signed areas rather than assumed.
2149    if let Some(crease) = crease {
2150        let start_vertex = take_id();
2151        let end_vertex = take_id();
2152        let crease_edge = take_id();
2153        result.vertices.push(VertexRecord { id: start_vertex, point: crease.start });
2154        result.vertices.push(VertexRecord { id: end_vertex, point: crease.end });
2155        let [crease_t0, crease_t1] = crease.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2156        result.edges.push(EdgeRecord {
2157            id: crease_edge,
2158            curve: crease.curve,
2159            t0: crease_t0,
2160            t1: crease_t1,
2161            start_vertex_id: start_vertex,
2162            end_vertex_id: end_vertex,
2163            degenerate: false,
2164            name: name.map(|value| format!("{value}:CREASE")),
2165        });
2166        let outer = crate::blend::chain::signed_area(&loops[0].coedges)?;
2167        let forward_first = crate::blend::chain::signed_area(&[
2168            CoedgeRecord { id: 0, edge_id: crease_edge, forward: true, pcurve: crease.first.clone() },
2169            CoedgeRecord { id: 0, edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2170        ])?;
2171        let inner = if outer * forward_first < 0.0 {
2172            vec![
2173                CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.first },
2174                CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.second.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2175            ]
2176        } else {
2177            vec![
2178                CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: true, pcurve: crease.second },
2179                CoedgeRecord { id: take_id(), edge_id: crease_edge, forward: false, pcurve: crease.first.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2180            ]
2181        };
2182        loops.push(LoopRecord { id: take_id(), coedges: inner });
2183    }
2184    let blend_face = FaceRecord {
2185        id: take_id(),
2186        surface: rows.surface,
2187        same_sense,
2188        loops,
2189        name: name.map(|value| value.to_string()),
2190    };
2191    let shell_index = result
2192        .shells
2193        .iter()
2194        .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2195        .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2196    result.shells[shell_index].faces.push(blend_face);
2197
2198    let used_edges: std::collections::HashSet<u64> = result
2199        .shells
2200        .iter()
2201        .flat_map(|shell| &shell.faces)
2202        .flat_map(|face| &face.loops)
2203        .flat_map(|loop_record| &loop_record.coedges)
2204        .map(|coedge| coedge.edge_id)
2205        .collect();
2206    result
2207        .edges
2208        .retain(|candidate| used_edges.contains(&candidate.id));
2209    let used_vertices: std::collections::HashSet<u64> = result
2210        .edges
2211        .iter()
2212        .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2213        .collect();
2214    result
2215        .vertices
2216        .retain(|candidate| used_vertices.contains(&candidate.id));
2217    // A surgery that had to REPAIR a neighbour trim took a branch no existing
2218    // fixture exercises, so it does not get the benefit of the doubt: prove the
2219    // result before returning it.  Paths that never repaired are untouched (the
2220    // check does not run), so this cannot slow or change anything that already
2221    // works — and the new branch can never ship a plausible-looking wrong solid
2222    // the way the un-repaired trim did.
2223    let consumed = SEAM_CONSUMED.with(|flag| flag.take());
2224    if repaired {
2225        let problems = result.validate_memoised();
2226        if !problems.is_empty() {
2227            // A setback that consumed a whole face and left a body that does
2228            // not stand is refused by name.
2229            if let (Some(face_id), true) = (consumed, crate::blend::setback::setback_consumed_face_refusal_on()) {
2230                return Err(crate::blend::setback::unsupported(
2231                    "setback_consumes_face",
2232                    format!(
2233                        "asymmetric chamfer: the setback on face {face_id} reaches that face's far edge, which \
2234                         would consume the whole face — refusing"
2235                    ),
2236                ));
2237            }
2238            return Err(KernelRefusal::internal(KernelStage::Validate, "neighbour_trim_closure", format!(
2239                "blend: the repaired neighbour trim did not close ({} issue(s), first: {})",
2240                problems.len(),
2241                problems
2242                    .first()
2243                    .map(|issue| issue.message.as_str())
2244                    .unwrap_or("unknown")
2245            )));
2246        }
2247    }
2248    Ok(result)
2249}
2250
2251thread_local! {
2252    /// The face whose seam a setback's trim shrank to a point during the
2253    /// surgery in progress (`replace_rim_in_face`), if any.
2254    static SEAM_CONSUMED: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
2255}
2256
2257/// Replace the blended edge's coedge in one mate's loop with the support
2258/// pieces (edge id, fit-space window, forward pcurve) in u order, and pull
2259/// the loop's seam-meridian pcurves that met the removed vertex down to the
2260/// support crossing at `uv_new` on this carrier. `true` when a neighbour trim
2261/// was repaired. The closed lane's own surgery and the rollover's share it.
2262pub(in crate::blend) fn replace_rim_in_face(
2263    result: &mut BrepSolid,
2264    edge: &EdgeRecord,
2265    mate: &BlendMate,
2266    pieces: &[(u64, [f64; 2], NurbsCurve)],
2267    uv_new: [f64; 2],
2268) -> Result<bool, KernelRefusal> {
2269    let old_vertex = edge.start_vertex_id;
2270    let v_new = uv_new[1];
2271    // (edge id, the START vertex is the removed one, the END vertex is).
2272    let seam_edge_ends: Vec<(u64, bool, bool)> = result
2273        .edges
2274        .iter()
2275        .filter(|candidate| {
2276            candidate.id != edge.id
2277                && (candidate.start_vertex_id == old_vertex
2278                    || candidate.end_vertex_id == old_vertex)
2279        })
2280        .map(|candidate| {
2281            (
2282                candidate.id,
2283                candidate.start_vertex_id == old_vertex,
2284                candidate.end_vertex_id == old_vertex,
2285            )
2286        })
2287        .collect();
2288    let seam_edge_ids: Vec<u64> = seam_edge_ends.iter().map(|(id, ..)| *id).collect();
2289    let pcurve_band = crate::KernelTolerances::for_solid(result, 1e-7).pcurve_consistency;
2290    let debug = std::env::var("BREP_DEBUG_NETWORK").is_ok();
2291    let face = result
2292        .shells
2293        .iter_mut()
2294        .flat_map(|shell| &mut shell.faces)
2295        .find(|face| face.id == mate.face.id)
2296        .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2297    let face_id = face.id;
2298    let FaceRecord { surface, loops, .. } = &mut *face;
2299    let loop_record = &mut loops[mate.loop_index];
2300    let position = loop_record
2301        .coedges
2302        .iter()
2303        .position(|coedge| coedge.edge_id == edge.id)
2304        .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
2305    let old_forward = loop_record.coedges[position].forward;
2306    let old_seam_v = {
2307        let pcurve = &loop_record.coedges[position].pcurve;
2308        let [d0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2309        pcurve.evaluate(d0).or_refuse(KernelStage::Refine, "evaluate")?.y
2310    };
2311    // Replacement coedges: pieces run in u order; a reversed loop use
2312    // takes them in reverse order, each reversed.
2313    let mut replacements = Vec::new();
2314    let ordered: Vec<&(u64, [f64; 2], NurbsCurve)> = if old_forward {
2315        pieces.iter().collect()
2316    } else {
2317        pieces.iter().rev().collect()
2318    };
2319    for (index, (edge_id, _, pcurve)) in ordered.iter().enumerate() {
2320        replacements.push(CoedgeRecord {
2321            id: if index == 0 {
2322                loop_record.coedges[position].id
2323            } else {
2324                0 // patched by the caller with fresh ids
2325            },
2326            edge_id: *edge_id,
2327            forward: old_forward,
2328            pcurve: if old_forward {
2329                pcurve.clone()
2330            } else {
2331                pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2332            },
2333        });
2334    }
2335    loop_record
2336        .coedges
2337        .splice(position..=position, replacements);
2338    let mut repaired = false;
2339    if loop_record.coedges.len() > 1 {
2340        // Anchored carrier: the loop's other coedges ride the seam
2341        // meridian between the old edge and the rest of the trim.
2342        // Pull the pcurve endpoint that met the old edge down to the
2343        // support crossing.  (The seam EDGE itself is trimmed after
2344        // both replacements, outside this borrow.)
2345        for coedge in &mut loop_record.coedges {
2346            if pieces
2347                .iter()
2348                .any(|(edge_id, ..)| *edge_id == coedge.edge_id)
2349                || !seam_edge_ids.contains(&coedge.edge_id)
2350            {
2351                continue;
2352            }
2353            let controls = &coedge.pcurve.control_points;
2354            let last_index = controls.len() - 1;
2355            let first_u = controls[0].x / controls[0].w;
2356            let last_u = controls[last_index].x / controls[last_index].w;
2357            let constant_u = (first_u - last_u).abs()
2358                <= 1e-9 * (1.0 + first_u.abs().max(last_u.abs()));
2359            // Which end of the pcurve met the removed vertex, and the uv
2360            // the crossing sits at on this carrier.
2361            let (target, target_uv, fraction) = if constant_u {
2362                // Seam MERIDIAN pcurve: constant u, runs along the v
2363                // direction; the endpoint at the removed edge's v moves
2364                // onto the support crossing.
2365                let first_v = controls[0].y / controls[0].w;
2366                let last_v = controls[last_index].y / controls[last_index].w;
2367                let target = if (first_v - old_seam_v).abs() < (last_v - old_seam_v).abs() {
2368                    0
2369                } else {
2370                    last_index
2371                };
2372                let span = last_v - first_v;
2373                let fraction = if span.abs() > 0.0 { (v_new - first_v) / span } else { 0.0 };
2374                (target, [first_u, v_new], fraction)
2375            } else {
2376                // The adjacent trim runs along U at constant v — the
2377                // carrier's OTHER (v) seam, which is what a closed edge
2378                // lying on one whole MERIDIAN borders (an axial-plane
2379                // collar on a torus).  Its endpoint moves in U, not V,
2380                // and the v-distance rule above cannot pick the end at
2381                // all: both ends share the same v, so it always lands on
2382                // `last_index`, which is wrong for one of the seam PAIR
2383                // (the v-seam edge appears twice in the loop, at v = 0
2384                // and v = 1).  Take the end from the TOPOLOGY.
2385                let Some((_, start_is_old, end_is_old)) = seam_edge_ends
2386                    .iter()
2387                    .find(|(id, ..)| *id == coedge.edge_id)
2388                    .copied()
2389                else {
2390                    continue;
2391                };
2392                if start_is_old == end_is_old {
2393                    return Err(KernelRefusal::ill_posed(KernelStage::Sew, "neighbour_trim_end", format!(
2394                        "blend: neighbour edge {} meets the blended edge at both ends; \
2395                         its trim end is ambiguous",
2396                        coedge.edge_id
2397                    )));
2398                }
2399                // `forward` maps the coedge's pcurve domain start onto the
2400                // edge's t0 (its start vertex).
2401                let target = if start_is_old == coedge.forward { 0 } else { last_index };
2402                let v_end = controls[target].y / controls[target].w;
2403                let span = last_u - first_u;
2404                let fraction = if span.abs() > 0.0 { (uv_new[0] - first_u) / span } else { 0.0 };
2405                (target, [uv_new[0], v_end], fraction)
2406            };
2407            // The trim is a SPLIT of the pcurve at the crossing's own
2408            // parameter, never a moved control point.  Moving the end
2409            // control point of a pcurve with interior knots re-slopes its
2410            // end span alone and breaks the pcurve ↔ edge parameter
2411            // correspondence validate reads (measured 2026-10-03 on the
2412            // torus-box-edge and axial-collar documents: the torus's
2413            // v-seam pcurve, a 3-span polyline, read 0.548 mm off its
2414            // edge at the same parameter while its image was on the edge
2415            // to 1e-10).  The seam-edge pass below projects the same
2416            // physical point onto the edge curve for the edge's own
2417            // parameter, so both sides keep the correspondence they had.
2418            let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2419            let target_point = surface
2420                .evaluate_extended(target_uv[0], target_uv[1])
2421                .or_refuse(KernelStage::Refine, "evaluate_extended")?;
2422            // Seeded at the AFFINE fraction of the pcurve's extent in the
2423            // moving direction (the seed `trim_edge_at` uses), not at the
2424            // pcurve's end: a crossing deep in a long seam would hand the
2425            // point solver a far seed and turn a blend that built into a
2426            // refusal.  The solver refines the seed to the physical point.
2427            let seed = (q0 + (q1 - q0) * fraction).clamp(q0.min(q1), q0.max(q1));
2428            let q_split = trim_parameter_at_point(
2429                surface, &coedge.pcurve, seed, target_point, pcurve_band,
2430            )?;
2431            let interior = q_split > q0 + 1e-9 * (q1 - q0) && q_split < q1 - 1e-9 * (q1 - q0);
2432            if debug {
2433                eprintln!(
2434                    "closed-edge seam trim: face {face_id} edge {} coedge {} ({}): {} at q {q_split:.9} of [{q0:.6}, {q1:.6}], {} control points",
2435                    coedge.edge_id, coedge.id, if constant_u { "meridian, constant u" } else { "along u" },
2436                    if interior { "SPLIT" } else { "EXTEND" }, controls.len()
2437                );
2438            }
2439            if interior {
2440                let (low, high) = coedge.pcurve.split(q_split).or_refuse(KernelStage::Refine, "split")?;
2441                coedge.pcurve = if target == 0 { high } else { low };
2442            } else {
2443                // The crossing lies at or past the pcurve's end: the seam
2444                // EXTENDS to meet it.  Exact for a two-point linear
2445                // pcurve, whose one span IS the whole parameterisation;
2446                // anything else would need a refit and is refused by name.
2447                if coedge.pcurve.degree != 1 || controls.len() != 2 {
2448                    return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_seam_extend_curved", format!(
2449                        "blend: neighbour seam edge {} would have to extend past its own end to meet the blend's support crossing, and its pcurve is not a single straight span (degree {}, {} control points)",
2450                        coedge.edge_id, coedge.pcurve.degree, controls.len()
2451                    )));
2452                }
2453                let w = coedge.pcurve.control_points[target].w;
2454                coedge.pcurve.control_points[target] = crate::Vec4::from_point(
2455                    Vec3::new(target_uv[0], target_uv[1], 0.0), w,
2456                );
2457            }
2458            // An asymmetric chamfer whose setback reaches the far end of this
2459            // seam consumes the whole face: the seam shrinks to a point. Noted
2460            // here; the surgery's own validation decides whether the body
2461            // stands (a ball's whole bore consumed does), and names the
2462            // refusal where it does not.
2463            if crate::blend::setback::active().is_some() {
2464                let [p0, p1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
2465                let (a, b) = (
2466                    coedge.pcurve.evaluate(p0).or_refuse(KernelStage::Refine, "evaluate")?,
2467                    coedge.pcurve.evaluate(p1).or_refuse(KernelStage::Refine, "evaluate")?,
2468                );
2469                if a.sub(b).length() <= 1e-12 * (1.0 + a.length()) {
2470                    SEAM_CONSUMED.with(|flag| flag.set(Some(face_id)));
2471                }
2472            }
2473            repaired = true;
2474        }
2475    }
2476    Ok(repaired)
2477}
2478
2479/// Trim the seam EDGES that ended on the removed vertex: each endpoint moves
2480/// onto the support crossing on ITS carrier — an edge of the first mate's
2481/// face to `first_target`, any other to `second_target`, each a (vertex,
2482/// seam-meridian parameter, point). `new_edge_ids` are the surgery's own
2483/// edges, skipped. `true` when a trim had to be re-projected (repaired).
2484pub(in crate::blend) fn trim_seam_edges(
2485    result: &mut BrepSolid,
2486    edge: &EdgeRecord,
2487    new_edge_ids: &[u64],
2488    first_face_edges: &[u64],
2489    first_target: (u64, f64, Vec3),
2490    second_target: (u64, f64, Vec3),
2491) -> Result<bool, KernelRefusal> {
2492    let old_vertex = edge.start_vertex_id;
2493    let mut repaired = false;
2494    let (vertex1_id, v1_new, cr_start) = first_target;
2495    let (second_seam_vertex, v2_new, second_seam_point) = second_target;
2496    for seam_edge in result.edges.iter_mut() {
2497        if new_edge_ids.contains(&seam_edge.id) {
2498            continue;
2499        }
2500        if seam_edge.start_vertex_id != old_vertex && seam_edge.end_vertex_id != old_vertex {
2501            continue;
2502        }
2503        let (target_vertex, target_v, target_point) = if first_face_edges.contains(&seam_edge.id) {
2504            (vertex1_id, v1_new, cr_start)
2505        } else {
2506            (second_seam_vertex, v2_new, second_seam_point)
2507        };
2508        let [c0, c1] = seam_edge.curve.domain().or_refuse(KernelStage::Refine, "domain")?;
2509        let clamped = target_v.clamp(c0.min(c1), c0.max(c1));
2510        // Using the support crossing's V as the neighbour's CURVE parameter is
2511        // only valid when that neighbour is the carrier's seam MERIDIAN, whose
2512        // 3D curve is parameterized by v.  Verify it against the vertex the
2513        // endpoint is moving to; a neighbour running along U instead (the
2514        // carrier's v-seam, bordered by a collar edge that lies on one whole
2515        // meridian) lands nowhere near it — the 2026-09-01 report trimmed the
2516        // torus' equator seam to u = 0, 10.885 away from its own vertex, and
2517        // the solid failed validation with "curve start does not match vertex".
2518        let band = 1e-6 * (1.0 + target_point.length());
2519        let split_parameter = if seam_edge.id == edge.id {
2520            // The BLENDED edge itself still carries the removed vertex at this
2521            // point.  Its coedges have already been replaced in both mates, so
2522            // the used-edge sweep below discards it; leave its (meaningless)
2523            // trim exactly as it was rather than asking a doomed edge to pass
2524            // through the support start.
2525            clamped
2526        } else {
2527            match seam_edge.curve.evaluate(clamped) {
2528                Ok(point) if point.sub(target_point).length() <= band => clamped,
2529                _ => {
2530                    repaired = true;
2531                    let projection =
2532                        crate::project_point_to_curve(&seam_edge.curve, target_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2533                    if projection.distance > band {
2534                        return Err(KernelRefusal::unsupported(KernelStage::Sew, "neighbour_off_support", format!(
2535                            "blend: neighbour edge {} does not pass through the blend's support \
2536                             start (off by {:.9}); the trim has no parameter to move to",
2537                            seam_edge.id, projection.distance
2538                        )));
2539                    }
2540                    projection.u.clamp(c0.min(c1), c0.max(c1))
2541                }
2542            }
2543        };
2544        if seam_edge.start_vertex_id == old_vertex {
2545            seam_edge.t0 = split_parameter;
2546            seam_edge.start_vertex_id = target_vertex;
2547        }
2548        if seam_edge.end_vertex_id == old_vertex {
2549            seam_edge.t1 = split_parameter;
2550            seam_edge.end_vertex_id = target_vertex;
2551        }
2552    }
2553    Ok(repaired)
2554}