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