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