Skip to main content

brep_kernel/blending/blend/edge/
open.rs

1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3
4/// General OPEN-edge rolling-ball fillet or chamfer: §4.9 march +
5/// §6.9 surgery with transverse edges on the two end faces.
6pub fn blend_open_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_open_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name)
20}
21
22/// `constant`: the radius when the blend came in through a CONSTANT-radius
23/// entry point (`blend_open_edge`), `None` for a variable profile — the only
24/// provenance on which the wall is judged against one rolling ball.
25pub(super) fn blend_open_edge_impl(
26    solid: &BrepSolid,
27    edge_id: u64,
28    radius_at: &dyn Fn(f64) -> f64,
29    constant: Option<f64>,
30    chamfer: bool,
31    name: Option<&str>,
32) -> Result<BrepSolid, KernelRefusal> {
33    let edge = solid
34        .edges
35        .iter()
36        .find(|edge| edge.id == edge_id)
37        .ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
38    if edge.start_vertex_id == edge.end_vertex_id {
39        return Err(KernelRefusal::input(KernelStage::Collect, "open_edge", "blend: blend_open_edge requires an open edge"));
40    }
41    let (first_face, first_loop, first_coedge) = locate_mate(solid, edge_id, None)?;
42    let (second_face, second_loop, second_coedge) =
43        locate_mate(solid, edge_id, Some((first_face.id, first_loop)))?;
44    let mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
45    let (rho1, rho2) = signed_radii(
46        edge,
47        first_face,
48        first_coedge,
49        second_face,
50        second_coedge,
51        mid_radius,
52    )?;
53    let first_mate = BlendMate {
54        face: first_face,
55        coedge: first_coedge,
56        loop_index: first_loop,
57        rho: rho1,
58    };
59    let second_mate = BlendMate {
60        face: second_face,
61        coedge: second_coedge,
62        loop_index: second_loop,
63        rho: rho2,
64    };
65    // End topology: boundary edges of both mates at each end vertex, the
66    // single face across the corner, and the support crossings.  When a
67    // prior fillet has already consumed one of the end corners, the support
68    // crossing can land at the very rim of the marched rows (the prior
69    // blend's transverse arc meets our contact line right at its base).
70    // March with a growing overshoot until every crossing lands strictly
71    // inside the fitted-row domain so the surgery can trim cleanly.
72    let debug = std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok();
73    if debug {
74        let sp = edge.curve.evaluate(edge.t0);
75        let ep = edge.curve.evaluate(edge.t1);
76        eprintln!(
77            "OPEN edge {} v{}->v{} first_face {} second_face {} p0={:?} p1={:?}",
78            edge.id,
79            edge.start_vertex_id,
80            edge.end_vertex_id,
81            first_face.id,
82            second_face.id,
83            sp,
84            ep
85        );
86    }
87    // The rung's MEASUREMENT (before anything is built on it), over what it
88    // ships: each rail against its carrier's extension over its own crossing
89    // window (`rails_on_shipped_windows`, at the network lane's rail bar, half
90    // `intersection_fit`), and a constant-radius FILLET's wall over those
91    // windows' envelope against the rolling ball's own sweep (the declared
92    // reader the chains use, at `intersection_fit`).
93    let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
94    let wall_bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
95    let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
96    // Constant-radius PROVENANCE (the entry point), never sampled constancy:
97    // a variable profile can agree at any finite set of samples.
98    let wall_radius = constant.filter(|radius| radius.is_finite() && *radius > 0.0);
99    let wall_check = !chamfer && wall_radius.is_some() && crate::blend::station_refinement_on();
100    let compute = |overshoot_fraction: f64, station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, bool, OpenRungReading), KernelRefusal> {
101        // The open-edge surgery locates its ends by support crossings, not by
102        // the marched vertex stations; the snapped indices only break the fit
103        // where the carriers' extension starts (`fit_open_rows`).
104        let law = crate::blend::setback::active();
105        let (stations, vertex_indices, refinement, dense) = if let Some(law) = law {
106            // An asymmetric chamfer's stations: the setback law's sections at
107            // the march's own parameters, refined by doubling to its bar.
108            let parameters = |count: usize| {
109                open_station_parameters(edge, overshoot_fraction, [edge.t0, edge.t1], count, [false, false])
110            };
111            let (stations, vertex_indices) = crate::blend::setback::march_open(
112                solid, edge, &first_mate, &second_mate, &law, station_count, &parameters, rail_bar,
113            )?;
114            let refinement = StationRefinement { exit: StationRefineExit::Converged, ..Default::default() };
115            (stations, vertex_indices, refinement, OpenDenseCentres::default())
116        } else { march_open_stations_core(
117            edge,
118            &first_mate,
119            &second_mate,
120            radius_at,
121            overshoot_fraction,
122            [edge.t0, edge.t1],
123            station_count,
124            [false, false],
125            [false, false],
126            crate::blend::station_refinement_on().then_some(rail_bar),
127            if wall_check { OPEN_WALL_CENTRES } else { 0 },
128            true,
129        )? };
130        let parameters = station_parameters(&stations);
131        let rows = fit_open_rows(
132            &stations,
133            &parameters,
134            chamfer,
135            Some(vertex_indices),
136            extrusion_direction(edge, &first_mate, &second_mate),
137        )?;
138        // The fitted wall itself must not double back along a row
139        // (`fold::check_rows_loop`); terminal, as the march's fold verdicts.
140        crate::blend::fold::check_rows_loop(
141            &rows, &stations, &parameters, Some(vertex_indices), radius_at(0.5 * (edge.t0 + edge.t1)), vec![first_mate.face.id, second_mate.face.id], crate::solid_scale(solid),
142        )?;
143        let mut ends = Vec::with_capacity(2);
144        let mut in_range = true;
145        for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
146            let (end, side_in_range) = resolve_free_end(
147                solid,
148                edge_id,
149                &first_mate,
150                &second_mate,
151                &rows,
152                vertex,
153                at_start,
154            )?;
155            if debug {
156                eprintln!(
157                    "  [os {overshoot_fraction:.2}] end v{vertex} at_start={at_start} end_face={} first_boundary={}(t={:.4}) second_boundary={}(t={:.4}) cr={:.4} cs={:.4} in_range={side_in_range}",
158                    end.end_face_id,
159                    end.first_edge_id,
160                    end.first_edge_parameter,
161                    end.second_edge_id,
162                    end.second_edge_parameter,
163                    end.cr_parameter,
164                    end.cs_parameter,
165                );
166            }
167            in_range &= side_in_range;
168            ends.push(end);
169        }
170        // The measurement covers what SHIPS: each rail over its own crossing
171        // window (the `cr`/`cs` parameters `build_open_surgery` trims it to,
172        // any retained overshoot included), and the wall over the envelope of
173        // both windows -- not merely between the vertex stations.
174        let windows = [
175            shipped_window(&parameters, ends[0].cr_parameter, ends[1].cr_parameter),
176            shipped_window(&parameters, ends[0].cs_parameter, ends[1].cs_parameter),
177        ];
178        let (rails, rail_samples) =
179            rails_on_shipped_windows(&rows, &parameters, &stations, [&first_mate.face.surface, &second_mate.face.surface], windows);
180        let walls = match (wall_check, windows) {
181            (false, _) => Vec::new(),
182            (true, [Some(first), Some(second)]) => open_wall_readings_why(
183                &rows,
184                &parameters,
185                &stations,
186                [first[0].min(second[0]), first[1].max(second[1])],
187                &dense,
188                wall_radius.unwrap_or(f64::NAN),
189            ),
190            // No shipped window to read: no wall coverage, never acceptance.
191            (true, _) => vec![Err("no shipped window to read".to_string())],
192        };
193        let first_unread = walls.iter().find_map(|wall| wall.as_ref().err().cloned());
194        let walls: Vec<Option<f64>> = if wall_check && windows.iter().any(|window| window.is_none()) {
195            Vec::new()
196        } else {
197            walls.into_iter().map(|wall| wall.ok()).collect()
198        };
199        let reading = OpenRungReading {
200            station_count,
201            stations: stations.len(),
202            rails,
203            rail_samples,
204            walls,
205            first_unread,
206            refine_exit: refinement.exit,
207            inserted: refinement.inserted,
208        };
209        Ok((rows, ends, in_range, reading))
210    };
211
212    // Radius-aware seeded first attempt (occt-filleting-system-study §6(b)
213    // lesson 7): OCCT floors corner extensions at 1.5·max_radius
214    // (`ExtentTwoCorner`) instead of probing blindly.  Convert that floor to
215    // an overshoot fraction of THIS edge: the contact rails sit r·tan(α/2)
216    // from the edge (α = sign-adjusted angle between the mates' raw normals
217    // at mid-edge — the march's own `cos_alpha`), so seed with 1.5× the
218    // widest end radius's rail offset over the edge arc length, clamped to
219    // the ladder's proven [0.08, 0.45] envelope.  Deterministic; `None` when
220    // it degenerates or merely reproduces the ladder's first rung.
221    let seeded_fraction: Option<f64> = (|| {
222        let span = edge.t1 - edge.t0;
223        let mut length = 0.0f64;
224        let mut previous = edge.curve.evaluate(edge.t0).ok()?;
225        for index in 1..=16 {
226            let t = edge.t0 + span * index as f64 / 16.0;
227            let point = edge.curve.evaluate(t).ok()?;
228            length += point.sub(previous).length();
229            previous = point;
230        }
231        if !(length > 0.0) || !length.is_finite() {
232            return None;
233        }
234        let mid_t = edge.t0 + span * 0.5;
235        let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
236        let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
237        let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
238        let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
239        let cos_alpha =
240            (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
241        // tan(α/2) = √((1−cosα)/(1+cosα)); the tangent-offset factor from
242        // the edge to each rail (box: α = π/2 → offset = r).
243        let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
244        let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
245        let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
246        if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
247            return None;
248        }
249        Some(fraction)
250    })();
251
252    // Retry with a growing overshoot; keep the first attempt whose crossings
253    // are all in range, else fall back to the widest march tried.  The seeded
254    // attempt is accepted ONLY when its crossings land in range — otherwise
255    // the blind ladder below runs exactly as before (including its
256    // fallback-to-first-Ok semantics), so the seed can improve the first
257    // landing but never change the fallback behaviour.
258    let choose = |station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, OpenRungReading), KernelRefusal> {
259    let mut chosen: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
260    let mut last_error: Option<KernelRefusal> = None;
261    // A wall that FOLDS is the shape's answer, not this rung's: every wider
262    // overshoot marches the same centre curve through the same bend, so the
263    // ladder is left at the first fold rather than re-proving it four times.
264    if let Some(fraction) = seeded_fraction {
265        match compute(fraction, station_count) {
266            Ok((rows, ends, true, reading)) => chosen = Some((rows, ends, reading)),
267            Ok(_) => {}
268            Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
269            Err(error) => last_error = Some(error),
270        }
271    }
272    if chosen.is_none() {
273        for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
274            match compute(overshoot_fraction, station_count) {
275                Ok((rows, ends, in_range, reading)) => {
276                    let fallback = chosen.is_none();
277                    if in_range {
278                        chosen = Some((rows, ends, reading));
279                        break;
280                    } else if fallback {
281                        chosen = Some((rows, ends, reading));
282                    }
283                }
284                Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
285                Err(error) => last_error = Some(error),
286            }
287        }
288    }
289    chosen.ok_or_else(|| {
290        last_error.unwrap_or_else(|| {
291            KernelRefusal::internal(
292                KernelStage::Refine,
293                "open_march",
294                "blend: open march failed at every overshoot",
295            )
296        })
297    })
298    };
299    // The LADDER: the open march's existing budget — STATIONS doubling to
300    // MAX_OPEN_STATIONS, each rung with its local refinement (REFINE_ROUNDS,
301    // REFINE_STATION_FACTOR) against the rail bar — climbed until a rung is
302    // ACCEPTED (rails and wall inside their bars), then, for a plain fillet,
303    // further toward `model` on the wall. A rung the request climbs to that
304    // fails acceptance is not taken: the accepted rung ships and says so,
305    // typed. A ladder that never accepts ships its top rung with a named note,
306    // as the network lane's open march does (a body that built before is not
307    // turned into a refusal here) -- but it is NOT an accepted rung: it ships
308    // with a typed `blend.wall_acceptance` report of its measured deficit.
309    // Acceptance is stated positively: every reading present, finite and
310    // inside its bar (a NaN or an empty coverage never passes).
311    let accepts = |reading: &OpenRungReading| {
312        reading.rails_read()
313            && reading.rails <= rail_bar
314            && (!wall_check || reading.worst_wall().is_some_and(|wall| wall <= wall_bar))
315    };
316    let meets_model = |reading: &OpenRungReading| reading.worst_wall().is_some_and(|wall| wall <= model);
317    let mut station_count = STATIONS;
318    let mut accepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
319    // The top rung of a ladder that NEVER accepted: shipped as built, kept
320    // apart from `accepted` so it is never reported as an accepted parent.
321    let mut unaccepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
322    let mut request_rungs = 0usize;
323    let mut unmet: Option<crate::BudgetReason> = None;
324    loop {
325        // A rung climbed to after acceptance is a request rung ATTEMPTED,
326        // whatever it returns.
327        if accepted.is_some() {
328            request_rungs += 1;
329        }
330        let chosen = choose(station_count);
331        let (rows, ends, reading) = match chosen {
332            Ok(chosen) => chosen,
333            // A request rung that cannot be built: the accepted rung stands.
334            Err(_) if accepted.is_some() => {
335                unmet = Some(crate::BudgetReason::Incoherent);
336                break;
337            }
338            Err(error) => return Err(error),
339        };
340        #[cfg(not(test))]
341        let reject_request = false;
342        crate::blend::carve::carve_trace(format_args!(
343            "blend open edge {}: {} stations a rung ({} in all), rails {:.3e} (bar {rail_bar:.3e}), wall {:.3e} (bar {wall_bar:.3e}){}",
344            edge.id, reading.station_count, reading.stations, reading.rails,
345            reading.worst_wall().unwrap_or(f64::INFINITY),
346            if accepted.is_some() { ", a request rung" } else { "" }
347        ));
348        let accepted_now = accepts(&reading) && !reject_request;
349        if accepted.is_none() {
350            if accepted_now {
351                let met = !wall_check || meets_model(&reading);
352                accepted = Some((rows, ends, reading));
353                if met {
354                    break;
355                }
356            } else if station_count < MAX_OPEN_STATIONS {
357                station_count *= 2;
358                continue;
359            } else {
360                crate::blend::record_blend_note(format!(
361                    "blend: open edge {} at {} stations a rung ({} in all) — the top of the open march's \
362                     ladder — still leaves its rails {:.3e} from their carriers (bar {rail_bar:.3e}) or its \
363                     wall off the rolling ball (bar {wall_bar:.3e}); the wall ships as built, unaccepted",
364                    edge.id, reading.station_count, reading.stations, reading.rails
365                ));
366                unaccepted = Some((rows, ends, reading));
367                break;
368            }
369        } else {
370            if !accepted_now {
371                unmet = Some(crate::BudgetReason::Rejected);
372                break;
373            }
374            let met = meets_model(&reading);
375            accepted = Some((rows, ends, reading));
376            if met {
377                unmet = None;
378                break;
379            }
380        }
381        // Accepted, short of `model`: one more rung if the budget allows.
382        if station_count < MAX_OPEN_STATIONS {
383            station_count *= 2;
384        } else {
385            unmet = Some(crate::BudgetReason::StationCeiling);
386            break;
387        }
388    }
389    let (shipped_accepted, (rows, ends, reading)) = match (accepted, unaccepted) {
390        (Some(rung), _) => (true, rung),
391        (None, Some(rung)) => (false, rung),
392        (None, None) => {
393            return Err(KernelRefusal::internal(KernelStage::Refine, "open_ladder", "blend: the open ladder ended without a rung"));
394        }
395    };
396    // What the shipped wall owes, typed: an UNACCEPTED rung its measured
397    // acceptance deficit (or that a reading could not be taken); an accepted
398    // fillet short of `model` its construction request.
399    let report: Option<(crate::BudgetReason, f64, f64, String)> = if !shipped_accepted {
400        Some(acceptance_deficit(&reading, rail_bar, wall_bar, wall_check, reading.rails_read(), "no rung of the open ladder was accepted"))
401    } else if wall_check && !meets_model(&reading) {
402        let reason = unmet.unwrap_or(crate::BudgetReason::StationCeiling);
403        // An accepted rung's every wall interval was read (acceptance needs it).
404        let declared = reading.worst_wall().unwrap_or(f64::INFINITY);
405        Some((reason, model, declared, String::new()))
406    } else {
407        None
408    };
409
410    let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
411        Ok(pair) => pair,
412        Err(_) => return Err(KernelRefusal::internal(KernelStage::Sew, "open_surgery_ends", "blend: open surgery needs exactly two ends")),
413    };
414    // A crossing that only resolved on a looser rung of the tolerance ladder
415    // builds something master refused outright, so it must earn its answer
416    // (see [`SpokeCrossings`]).
417    let mut crossings = SpokeCrossings::default();
418    crossings.note(start_end.escalated || finish_end.escalated);
419    let mut result = solid.clone();
420    let mut take_id = fresh_id_source(solid);
421    let sewn = build_open_surgery(
422        solid,
423        &mut result,
424        &mut take_id,
425        edge,
426        &first_mate,
427        &second_mate,
428        rows,
429        [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
430        name,
431    )?;
432    prune_orphan_vertices(&mut result);
433    // Extended end boundaries can leave a finite planar chart. Rebuild those
434    // charts before validating the surgery, rather than rejecting a correct
435    // edge because its projected pcurve was clamped to the old rectangle.
436    crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
437    check_snap_closure(solid, &result, sewn.snap)?;
438    let built = crossings.gate(result)?;
439    // The wall AS SEWN: the surgery may transpose the face (an exact
440    // extrusion), and a report is matched to its face by exact surface.
441    let sewn_surface = built
442        .shells
443        .iter()
444        .flat_map(|shell| &shell.faces)
445        .find(|face| face.id == sewn.blend_face_id)
446        .map(|face| face.surface.clone())
447        .ok_or(KernelRefusal::internal(KernelStage::Sew, "open_blend_face", "blend: the sewn open blend face is not in the built body"))?;
448    // A wall that SHIPS unaccepted, or short of its construction request,
449    // says so, typed, on the built body's wall (exact name and sewn surface).
450    if let Some((reason, bar, measured, deficit)) = report {
451        // The budget is the open march's GLOBAL station ladder (STATIONS
452        // doubling to MAX_OPEN_STATIONS), not local rounds: for a request,
453        // the rungs ATTEMPTED past the accepted one (a refused one included);
454        // for an unaccepted rung, the rungs climbed past the first. Each rung
455        // also ran its own local rail refinement, named in the detail.
456        let rungs_used = if shipped_accepted { request_rungs } else { (reading.station_count / STATIONS).trailing_zeros() as usize };
457        let mut detail = format!(
458            "the shipped rung's local rail refinement ended {:?} with {} stations inserted",
459            reading.refine_exit, reading.inserted
460        );
461        if !deficit.is_empty() {
462            detail = format!("{deficit}; {detail}");
463        }
464        crate::blend::record_wall_model_report(crate::blend::WallModelReport {
465            name: name.map(str::to_string),
466            surface: sewn_surface,
467            request: bar,
468            residual: measured,
469            detail: Some(detail),
470            budget: crate::ApproximationBudget {
471                reason,
472                rounds_used: rungs_used,
473                rounds_limit: OPEN_REQUEST_RUNGS,
474                stations: reading.stations,
475                station_limit: REFINE_STATION_FACTOR * (MAX_OPEN_STATIONS + 1),
476                mechanism: crate::BudgetMechanism::StationRungs,
477                measured_component: crate::MeasuredComponent::Wall,
478                unread: 0,
479            },
480        });
481    }
482    Ok(built)
483}
484
485/// An UNACCEPTED shipped rung's deficit, typed: `Unread` when any reading
486/// acceptance needs could not be taken (a non-finite rail, an empty shipped
487/// window, an unread or missing wall interval), else `Unaccepted`; the
488/// measured value and its bar are the readable component worst against its
489/// bar (rails against `rail_bar`, the wall against `wall_bar`; 0 against
490/// `rail_bar` when nothing could be read), always finite. The text names both
491/// readings against their bars, after `what` (what failed acceptance: the
492/// open ladder's every rung, or a network stripe's shipped windows).
493/// `rails_read` is the caller's verdict on whether the rails were READ (the
494/// open lane: [`OpenRungReading::rails_read`]; a network stripe also counts a
495/// validated collapsed rail as read with no samples).
496pub(in crate::blend) fn acceptance_deficit(
497    reading: &OpenRungReading,
498    rail_bar: f64,
499    wall_bar: f64,
500    wall_check: bool,
501    rails_read: bool,
502    what: &str,
503) -> (crate::BudgetReason, f64, f64, String) {
504    let rails = (rails_read && reading.rails.is_finite() && reading.rails >= 0.0).then_some(reading.rails);
505    let wall = if wall_check { reading.worst_wall() } else { None };
506    let unread = rails.is_none() || (wall_check && wall.is_none());
507    let mut worst = (0.0_f64, rail_bar);
508    for (value, bar) in [(rails, rail_bar), (wall, wall_bar)] {
509        if let Some(value) = value {
510            if value / bar > worst.0 / worst.1 {
511                worst = (value, bar);
512            }
513        }
514    }
515    let rail_text = match rails {
516        Some(value) => format!("rails {value:.3e} from their carriers (bar {rail_bar:.3e})"),
517        None => format!(
518            "rails UNREAD ({:?} over {} + {} samples) against bar {rail_bar:.3e}",
519            reading.rails, reading.rail_samples[0], reading.rail_samples[1]
520        ),
521    };
522    let wall_text = match (wall_check, wall) {
523        (false, _) => "the wall not judged (no constant-radius fillet provenance)".to_string(),
524        (true, Some(value)) => format!("wall {value:.3e} off the rolling ball (bar {wall_bar:.3e})"),
525        (true, None) => format!(
526            "wall UNREAD on {} of {} intervals against bar {wall_bar:.3e}{}",
527            reading.walls.iter().filter(|wall| !matches!(wall, Some(value) if value.is_finite())).count(),
528            reading.walls.len(),
529            reading.first_unread.as_deref().map(|why| format!(" (first: {why})")).unwrap_or_default()
530        ),
531    };
532    let reason = if unread { crate::BudgetReason::Unread } else { crate::BudgetReason::Unaccepted };
533    (reason, worst.1, worst.0, format!("{what}: {rail_text}, {wall_text}"))
534}
535
536/// Section probes per open wall interval (k/8 of the section), and centres
537/// per interval (probes at the even ones), as the chains read their walls.
538const OPEN_WALL_SECTIONS: usize = 8;
539pub(in crate::blend) const OPEN_WALL_CENTRES: usize = 32;
540/// The rungs the construction request may climb past the first accepted one:
541/// the open march's own ladder, STATIONS doubling to MAX_OPEN_STATIONS.
542pub(in crate::blend) const OPEN_REQUEST_RUNGS: usize = (MAX_OPEN_STATIONS / STATIONS).trailing_zeros() as usize;
543
544/// One open rung's measurement over what it SHIPS: its rung, its station
545/// count (refinement included), the rails' worst distance from their
546/// carriers over each rail's own crossing window and the samples read on
547/// each (0: no window), each wall interval's declared reading (`None`:
548/// unreadable) over the windows' envelope, and how the rung's own LOCAL rail
549/// refinement ended (its exit and the stations it inserted).
550#[derive(Clone, Debug)]
551pub(in crate::blend) struct OpenRungReading {
552    pub(in crate::blend) station_count: usize,
553    pub(in crate::blend) stations: usize,
554    pub(in crate::blend) rails: f64,
555    pub(in crate::blend) rail_samples: [usize; 2],
556    pub(in crate::blend) walls: Vec<Option<f64>>,
557    /// The first unread wall interval and why (diagnostic; `None` when every
558    /// interval read or the wall was not judged).
559    pub(in crate::blend) first_unread: Option<String>,
560    pub(in crate::blend) refine_exit: crate::blend::stations::StationRefineExit,
561    pub(in crate::blend) inserted: usize,
562}
563
564impl OpenRungReading {
565    /// Every rail READ: a finite, nonnegative worst distance from samples on
566    /// both rails' shipped windows. A NaN, an infinity (an unreadable foot)
567    /// or an empty window is never a reading.
568    pub(in crate::blend) fn rails_read(&self) -> bool {
569        self.rails.is_finite() && self.rails >= 0.0 && self.rail_samples.iter().all(|&samples| samples > 0)
570    }
571
572    /// Every wall interval READ (and at least one): `None` when any is not.
573    pub(in crate::blend) fn worst_wall(&self) -> Option<f64> {
574        if self.walls.is_empty() {
575            return None;
576        }
577        self.walls.iter().try_fold(0.0_f64, |worst, wall| match wall {
578            Some(declared) if declared.is_finite() && *declared >= 0.0 => Some(worst.max(*declared)),
579            _ => None,
580        })
581    }
582}
583
584/// The shipped window `[low, high]` of one rail between its two crossing
585/// parameters, within the rows' parameters (the surgery refuses a crossing
586/// outside them; the reading only never indexes past them). `None` when the
587/// window is empty or not finite: nothing ships there that could be read.
588pub(in crate::blend) fn shipped_window(parameters: &[f64], a: f64, b: f64) -> Option<[f64; 2]> {
589    let (&first, &last) = (parameters.first()?, parameters.last()?);
590    let (low, high) = (a.min(b).max(first), a.max(b).min(last));
591    (low.is_finite() && high.is_finite() && low < high).then_some([low, high])
592}
593
594/// The station interval holding `u` and `u`'s fraction of it.
595fn interval_at(parameters: &[f64], u: f64) -> Option<(usize, f64)> {
596    let last = parameters.len().checked_sub(2)?;
597    let interval = parameters.partition_point(|&parameter| parameter <= u).saturating_sub(1).min(last);
598    let (from, to) = (parameters[interval], parameters[interval + 1]);
599    (to > from).then(|| (interval, ((u - from) / (to - from)).clamp(0.0, 1.0)))
600}
601
602/// Lane R2 row 2a (2026-10-18): `BREP_BLEND_AFFINE_READS=1`/`on` reads the
603/// rails of a straight stripe on a PLANE at their window ends
604/// ([`affine_rail_standoff`], [`exact_planar_trim`]'s read; the default since
605/// its census: 98 + 144 cases identical in every field), `=0`/`off` samples
606/// them as before; `set_affine_reads_for_test` forces it on a test's own
607/// thread.
608const AFFINE_READS_DEFAULT: bool = true;
609
610pub(in crate::blend) fn affine_reads_on() -> bool {
611    switch!(live "BREP_BLEND_AFFINE_READS", AFFINE_READS_DEFAULT)
612}
613
614
615/// A point of `surface` and its unit normal when the surface is AFFINE in
616/// (u, v), its extension included: a plane (`analytic`) of degree 1 × 1 whose
617/// 2 × 2 net is a parallelogram with equal weights, to `1e-12·(1 + scale)`.
618/// Every point of such a surface, and of its linear extension, lies on the one
619/// plane through the corner with that normal.
620fn affine_plane(surface: &NurbsSurface) -> Option<(Vec3, Vec3)> {
621    let (origin, _, _) = affine_plane_frame(surface)?;
622    // The normal off the net's own corner differences.
623    let net = &surface.control_points;
624    let [p00, p01, p10] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?];
625    Some((origin, p10.sub(p00).cross(p01.sub(p00)).normalized().ok()?))
626}
627
628/// [`affine_plane`]'s surface as `S(u, v) = origin + (u − u₀)·s_u + (v − v₀)·s_v`:
629/// the net's corner and the two constant partial derivatives.
630pub(in crate::blend) fn affine_plane_frame(surface: &NurbsSurface) -> Option<(Vec3, Vec3, Vec3)> {
631    if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || surface.degree_u != 1 || surface.degree_v != 1 {
632        return None;
633    }
634    let net = &surface.control_points;
635    if net.len() != 2 || net.iter().any(|row| row.len() != 2) {
636        return None;
637    }
638    let weight = net[0][0].w;
639    if !(weight > 0.0) || net.iter().flatten().any(|point| point.w != weight) {
640        return None;
641    }
642    let [p00, p01, p10, p11] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?, net[1][1].point().ok()?];
643    let scale = [p00, p01, p10, p11].iter().fold(0.0f64, |worst, point| worst.max(point.length()));
644    if p11.sub(p10).sub(p01.sub(p00)).length() > 1e-12 * (1.0 + scale) {
645        return None;
646    }
647    let [u0, u1] = surface.domain_u().ok()?;
648    let [v0, v1] = surface.domain_v().ok()?;
649    if !(u1 > u0 && v1 > v0) {
650        return None;
651    }
652    Some((p00, p10.sub(p00).scale(1.0 / (u1 - u0)), p01.sub(p00).scale(1.0 / (v1 - v0))))
653}
654
655/// Whether `curve` is AFFINE in its parameter: degree 1, two control points of
656/// equal weight (a `make_line`).
657fn affine_curve(curve: &NurbsCurve) -> bool {
658    curve.degree == 1
659        && curve.control_points.len() == 2
660        && curve.control_points[0].w > 0.0
661        && curve.control_points[0].w == curve.control_points[1].w
662}
663
664/// The worst distance of an affine rail from an affine carrier over `window`,
665/// read at the window's two ends: the signed distance is affine in the rail
666/// parameter, so its magnitude is convex and its maximum is at an end. `None`
667/// when either is not affine (the sampled reader's), or an end will not
668/// evaluate.
669pub(in crate::blend) fn affine_rail_standoff(rail: &NurbsCurve, carrier: &NurbsSurface, window: [f64; 2]) -> Option<f64> {
670    if !affine_curve(rail) {
671        return None;
672    }
673    let (origin, normal) = affine_plane(carrier)?;
674    let mut worst = 0.0f64;
675    for u in window {
676        let point = rail.evaluate(u).ok()?;
677        let off = point.sub(origin).dot(normal).abs();
678        if !off.is_finite() {
679            return None;
680        }
681        worst = worst.max(off);
682    }
683    Some(worst)
684}
685
686/// Lane N row 4 (2026-10-28): the exact revolution rows' reads in closed
687/// form. `BREP_EXACT_REVOLUTION_READS=1`/`on` turns them on, `=0`/`off`
688/// off; ON by default since lane N's promotion (2026-10-28).
689const EXACT_REVOLUTION_READS_DEFAULT: bool = true;
690
691pub(in crate::blend) fn exact_revolution_reads_on() -> bool {
692    switch!(live "BREP_EXACT_REVOLUTION_READS", EXACT_REVOLUTION_READS_DEFAULT)
693}
694
695
696/// A bound on `|S(pcurve(s)) − rail(s)|` over the whole parameter range when
697/// the shipped pair is an exact revolution row's (`exact_revolution_rows`):
698///
699/// * on the REVOLUTION carrier the pcurve is a uv line along one coordinate
700///   and the rail is the carrier's parallel circle at the held coordinate,
701///   continued to the line's range along its own conic pieces
702///   (`extend_conic_ends`) and re-parameterised to `[0, 1]`; that curve is
703///   rebuilt here from the pcurve's two ends and compared with the rail net
704///   for net — identical bases, so the homogeneous control differences
705///   bound the curves everywhere (`net_bound`);
706/// * on an AFFINE plane the pcurve carries the rail's own knots and the
707///   chart images of the rail's control points; the plane's image of the
708///   pcurve net is compared with the rail net the same way.
709///
710/// `None` when the pair is neither (the sampled read runs). The rail's
711/// standoff from the carrier and the pcurve's miss are each at most the
712/// bound (a point's distance to a surface is at most its distance to any
713/// point of it; the miss adds the standoff once more), so a bound within a
714/// third of the pcurve floor stands the trim by the march trim's own rule.
715pub(in crate::blend) fn exact_revolution_read_bound(rail: &NurbsCurve, carrier: &NurbsSurface, pcurve: &NurbsCurve) -> Option<f64> {
716    if let Some((origin, s_u, s_v)) = affine_plane_frame(carrier) {
717        // The plane's image of the pcurve net, weight for weight.
718        if pcurve.degree != rail.degree || pcurve.knots != rail.knots || pcurve.control_points.len() != rail.control_points.len() {
719            return None;
720        }
721        let ([u0, _], [v0, _]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
722        let image: Vec<Vec4> = pcurve
723            .control_points
724            .iter()
725            .map(|control| {
726                let (u, v) = (control.x / control.w, control.y / control.w);
727                let point = origin.add(s_u.scale(u - u0)).add(s_v.scale(v - v0));
728                Vec4 { x: point.x * control.w, y: point.y * control.w, z: point.z * control.w, w: control.w }
729            })
730            .collect();
731        return crate::blend::stations::net_bound(&rail.control_points, &image);
732    }
733    if !matches!(carrier.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. } | crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. } | crate::AnalyticSurface::Sphere { .. })) {
734        return None;
735    }
736    if !affine_curve(pcurve) {
737        return None;
738    }
739    let [q0, q1] = pcurve.domain().ok()?;
740    let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
741    let ([u0, u1], [v0, v1]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
742    let (circle, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x && a.y >= v0 && a.y <= v1 {
743        (carrier.iso_curve_v(a.y).ok()?, a.x, b.x)
744    } else if a.x.to_bits() == b.x.to_bits() && a.y != b.y && a.x >= u0 && a.x <= u1 {
745        (carrier.iso_curve_u(a.x).ok()?, a.y, b.y)
746    } else {
747        return None;
748    };
749    let [c0, c1] = circle.domain().ok()?;
750    let reversed = to < from;
751    let circle = if reversed { circle.reversed().ok()? } else { circle };
752    let on_curve = |along: f64| if reversed { c0 + c1 - along } else { along };
753    let (lo, hi) = (on_curve(from), on_curve(to));
754    if !(hi > lo) {
755        return None;
756    }
757    let segment = super::support::extend_conic_ends(&circle, lo, hi).ok()?;
758    let [r0, r1] = segment.domain().ok()?;
759    let degree = segment.degree;
760    let mut knots = segment.knots.clone();
761    for knot in knots.iter_mut() {
762        *knot = ((*knot - r0) / (r1 - r0)).clamp(0.0, 1.0);
763    }
764    for knot in knots.iter_mut().take(degree + 1) {
765        *knot = 0.0;
766    }
767    let count = knots.len();
768    for knot in knots.iter_mut().skip(count - degree - 1) {
769        *knot = 1.0;
770    }
771    if rail.degree != degree || rail.knots != knots || rail.control_points.len() != segment.control_points.len() {
772        return None;
773    }
774    crate::blend::stations::net_bound(&rail.control_points, &segment.control_points)
775}
776
777/// Each shipped rail's worst distance from its carrier over ITS OWN window
778/// ([`shipped_window`]): both window ends exactly and the quarter points of
779/// every station span inside it, each against the carrier's EXTENSION
780/// (`extended_foot_distance`, seeded by the stations' own contact uv) --
781/// a finite planar chart clamps its projection, so retained overshoot past a
782/// vertex would read a miss that is not there. `(worst, samples per rail)`;
783/// any unreadable sample (a failed evaluation, an infinite foot) makes the
784/// worst NaN, which [`OpenRungReading::rails_read`] never admits.
785pub(in crate::blend) fn rails_on_shipped_windows(
786    rows: &FittedRows,
787    parameters: &[f64],
788    stations: &[Station],
789    carriers: [&NurbsSurface; 2],
790    windows: [Option<[f64; 2]>; 2],
791) -> (f64, [usize; 2]) {
792    read_rail_windows([&rows.cr, &rows.cs], parameters, stations, carriers, windows,
793        rows.exact_extrusion && affine_reads_on(), certified_rail_bounds(rows, carriers), None)
794}
795
796/// An exact revolution row's rails, bounded in closed form
797/// (`exact_revolution_read_bound`) when the reads lane is on: the standoff
798/// over any window is at most the bound.
799fn certified_rail_bounds(rows: &FittedRows, carriers: [&NurbsSurface; 2]) -> [Option<f64>; 2] {
800    if rows.revolution_axis.is_none() || !exact_revolution_reads_on() {
801        return [None, None];
802    }
803    [
804        exact_revolution_read_bound(&rows.cr, carriers[0], &rows.cr_pcurve),
805        exact_revolution_read_bound(&rows.cs, carriers[1], &rows.cs_pcurve),
806    ]
807}
808
809fn read_rail_windows(
810    rails: [&NurbsCurve; 2], parameters: &[f64], stations: &[Station],
811    carriers: [&NurbsSurface; 2], windows: [Option<[f64; 2]>; 2], affine: bool,
812    certified: [Option<f64>; 2],
813    mut memo: Option<&mut RailFeet>,
814) -> (f64, [usize; 2]) {
815    let mut worst = 0.0_f64;
816    let mut samples = [0usize; 2];
817    for (side, (rail, carrier)) in [(rails[0], carriers[0]), (rails[1], carriers[1])].into_iter().enumerate() {
818        let Some([low, high]) = windows[side] else { continue };
819        // An exact extrusion's straight rail on a plane: the ends carry the
820        // worst (`affine_rail_standoff`), the samples between them cannot.
821        if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
822            crate::blend::record_count("count.read.affine_rails", 1);
823            samples[side] += 2;
824            if !worst.is_nan() && off > worst {
825                worst = off;
826            }
827            continue;
828        }
829        // An exact revolution row's rail: its net bound against the carrier's
830        // own circle (or the plane's image of its pcurve) is its standoff's
831        // bound over every window.
832        if let Some(bound) = certified[side] {
833            crate::blend::record_count("count.read.revolution_rails", 1);
834            samples[side] += 1;
835            if !worst.is_nan() && bound > worst {
836                worst = bound;
837            }
838            continue;
839        }
840        let mut at = vec![low, high];
841        for pair in parameters.windows(2) {
842            for fraction in [0.25, 0.5, 0.75] {
843                let u = pair[0] + (pair[1] - pair[0]) * fraction;
844                if u > low && u < high {
845                    at.push(u);
846                }
847            }
848        }
849        for u in at {
850            let cached = memo.as_ref().and_then(|memo| memo.values[side].get(&u.to_bits()).copied());
851            let off = if let Some(off) = cached {
852                if let Some(memo) = memo.as_mut() { memo.hits += 1; }
853                crate::blend::record_count("count.read.rail_foot_reused", 1);
854                off
855            } else {
856            if let Some(memo) = memo.as_mut() {
857                memo.reads += 1;
858                crate::blend::record_count("count.read.rail_foot_fresh", 1);
859            }
860            let off = interval_at(parameters, u)
861                .and_then(|(interval, fraction)| {
862                    let (left, right) = (&stations[interval], &stations[interval + 1]);
863                    let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
864                    let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
865                    let point = rail.evaluate(u).ok()?;
866                    crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
867                })
868                .filter(|off| off.is_finite())
869                .unwrap_or(f64::NAN);
870            if off.is_finite() {
871                if let Some(memo) = memo.as_mut() { memo.values[side].insert(u.to_bits(), off); }
872            }
873            off
874            };
875            samples[side] += 1;
876            if !worst.is_nan() && (off.is_nan() || off > worst) {
877                worst = off;
878            }
879        }
880    }
881    (worst, samples)
882}
883
884/// A construction-local rail reader. It owns an immutable snapshot of every
885/// input to a foot reading; only the requested windows can change. Native
886/// parameter bits identify a sample, so nearby endpoints never share a foot.
887/// The network keeps this reader from its chosen rung through shipped-window
888/// acceptance, then drops it. Failed/nonfinite feet are never reused.
889pub(in crate::blend) struct RailWindowReader {
890    rails: [NurbsCurve; 2],
891    carriers: [NurbsSurface; 2],
892    parameters: Vec<f64>,
893    stations: Vec<Station>,
894    affine: bool,
895    certified: [Option<f64>; 2],
896    feet: RailFeet,
897}
898
899#[derive(Default)]
900struct RailFeet {
901    values: [rustc_hash::FxHashMap<u64, f64>; 2],
902    reads: u64,
903    hits: u64,
904}
905
906impl RailWindowReader {
907    pub(in crate::blend) fn new(rows: &FittedRows, parameters: &[f64], stations: &[Station], carriers: [&NurbsSurface; 2]) -> Self {
908        Self {
909            rails: [rows.cr.clone(), rows.cs.clone()],
910            carriers: [carriers[0].clone(), carriers[1].clone()],
911            parameters: parameters.to_vec(), stations: stations.to_vec(),
912            affine: rows.exact_extrusion && affine_reads_on(), certified: certified_rail_bounds(rows, carriers), feet: RailFeet::default(),
913        }
914    }
915
916    pub(in crate::blend) fn read(&mut self, windows: [Option<[f64; 2]>; 2]) -> (f64, [usize; 2]) {
917        read_rail_windows([&self.rails[0], &self.rails[1]], &self.parameters, &self.stations,
918            [&self.carriers[0], &self.carriers[1]], windows, self.affine, self.certified, Some(&mut self.feet))
919    }
920}
921
922pub(in crate::blend) fn network_rail_reuse_on() -> bool {
923    // The private halving diagnostic compares every requested foot against
924    // its reference algorithm, including repeats. Preserve that coverage.
925    std::env::var("BREP_NETWORK_RAIL_REUSE").map_or(true, |value| value != "0" && value != "off")
926}
927
928/// The JUNCTION stencil across the station between interval `left` and
929/// `left + 1`: `left_nodes` (33 exact centres of `left`) from node 16 to 32,
930/// then `right_nodes` (33 of `left + 1`) from node 1 to 16, the shared
931/// station at node 16. Its native abscissae come from `station_ts` (the
932/// edge parameters the centres were solved at): `None` when the two native
933/// widths are bitwise equal -- uniform nodes, read by the ordinary reader --
934/// else each node's t, interior nodes by the dense solves' own arithmetic
935/// t_i + (t_{i+1} − t_i)·j/32 and the shared station at its STORED t. Err
936/// when a station t is not finite or a width is not strictly positive.
937pub(in crate::blend) fn junction_stencil(
938    left_nodes: &[Vec3],
939    right_nodes: &[Vec3],
940    station_ts: &[f64],
941    left: usize,
942) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
943    let half = OPEN_WALL_CENTRES / 2;
944    if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
945        return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
946    }
947    let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
948    let (w_left, w_right) = (t1 - t0, t2 - t1);
949    if !(t0.is_finite() && t1.is_finite() && t2.is_finite() && w_left > 0.0 && w_right > 0.0 && w_left.is_finite() && w_right.is_finite()) {
950        return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
951    }
952    let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
953    if w_left.to_bits() == w_right.to_bits() {
954        return Ok((nodes, None));
955    }
956    let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
957        .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
958        .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
959        .collect();
960    Ok((nodes, Some(abscissae)))
961}
962
963/// One wall probe on a stencil: the ordinary uniform reader when it has no
964/// native abscissae (an interval's own nodes, or a junction of equal native
965/// widths), the native-abscissa reader when it has them.
966pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
967    match abscissae {
968        None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
969        Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
970    }
971}
972
973/// Every interval's largest DECLARED wall reading (`chain::declared_wall_probe`,
974/// reading + uncertainty) on the open fillet `rows`, against the exact ball
975/// centres the march solved at j/32 of each interval ([`OpenDenseCentres`]),
976/// or, for each unread interval, WHY: its station
977/// interval and which component failed (no dense row, a dense row of the
978/// wrong size, an exact centre the march did not solve, a wall point the
979/// surface would not evaluate, a probe the declared reader declined, no probe
980/// in the shipped part, or a non-finite reading). The cause is diagnostic.
981///
982/// Read on the DECLARED SHIPPED REGION `window` only: an interval it overlaps
983/// is probed at its even stencil fractions that lie inside the window and at
984/// each window end inside the interval; a probe outside the window is not on
985/// the shipped wall and is not read. A window END whose nearest even stencil
986/// node falls outside 2..=30 -- an end within a node of the interval's own
987/// station, as the original bore's shipped spans were, 1e-16 from their
988/// vertex stations -- has its foot at the interval's end node, where
989/// [`crate::blend::chain::declared_wall_probe`] cannot read (the stencil
990/// cannot follow a foot out of its own interval). It is read on a JUNCTION
991/// stencil instead: the neighbouring interval's last half and this one's
992/// first half (or this one's last half and the next one's first), the same
993/// exact centres meeting at the shared station, with the probe at its
994/// middle node. Two intervals of unequal native width (local insertions)
995/// give the junction unequal node spacing, so it is then read by the
996/// native-abscissa reader (`chain::declared_wall_probe_at_why`) on the
997/// edge parameters the centres were solved at; of equal width, by the
998/// ordinary reader. With no neighbour it is read as before.
999/// The wall reading of an EXACT extrusion's rows, read directly: the wall is
1000/// the ball's sweep along the centre line, so every point of the shipped
1001/// surface stands exactly the radius off that line, and the reading is how
1002/// far it does not. One entry per interval overlapping `window`, probes at
1003/// the interval's quarters inside the window and at the window's ends, over
1004/// the section at k/8 (the ball-resolve reader's sections). Never for fitted
1005/// rows: those are read against the exact centres between their stations
1006/// ([`open_wall_readings_why`]).
1007pub(in crate::blend) fn exact_extrusion_wall_readings(
1008    rows: &FittedRows,
1009    parameters: &[f64],
1010    window: [f64; 2],
1011    radius: f64,
1012) -> Vec<Result<f64, String>> {
1013    let intervals = parameters.len().saturating_sub(1);
1014    let overlapped: Vec<usize> =
1015        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1016    let line = match (rows.exact_extrusion, rows.center.as_ref()) {
1017        (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
1018        (false, _) => Err("the rows are not an exact extrusion".to_string()),
1019        (true, None) => Err("the exact rows carry no centre line".to_string()),
1020    };
1021    let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
1022        Ok(line) => line,
1023        Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
1024    };
1025    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1026        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1027    };
1028    overlapped
1029        .into_iter()
1030        .map(|interval| -> Result<f64, String> {
1031            let (from, to) = (parameters[interval], parameters[interval + 1]);
1032            let inside = |u: f64| u >= window[0] && u <= window[1];
1033            let mut probes: Vec<f64> =
1034                (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
1035            probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
1036            if probes.is_empty() {
1037                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1038            }
1039            let mut worst = 0.0_f64;
1040            for u in probes {
1041                for k in 1..OPEN_WALL_SECTIONS {
1042                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1043                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1044                    let offset = point.sub(anchor);
1045                    let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
1046                    let reading = (standoff - radius).abs();
1047                    if !reading.is_finite() {
1048                        return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
1049                    }
1050                    worst = worst.max(reading);
1051                }
1052            }
1053            Ok(worst)
1054        })
1055        .collect()
1056}
1057
1058/// The wall reading of an EXACT REVOLUTION's rows
1059/// (`support::exact_revolution_rows`), read directly: the wall is the ball's
1060/// sweep along the centre CIRCLE, so every point of the shipped surface
1061/// stands exactly the radius off that circle, and the reading is how far it
1062/// does not. The distance to the circle is closed-form in the point's
1063/// meridian half-plane: `hypot(ρ − R_c, h − h_c)` for the point's radial
1064/// distance ρ and axial height h about the rows' axis. Same probes as
1065/// [`exact_extrusion_wall_readings`]: one entry per interval overlapping
1066/// `window`, the interval's quarters inside the window and the window's
1067/// ends, over the section at k/8.
1068pub(in crate::blend) fn exact_revolution_wall_readings(
1069    rows: &FittedRows,
1070    parameters: &[f64],
1071    window: [f64; 2],
1072    radius: f64,
1073) -> Vec<Result<f64, String>> {
1074    let intervals = parameters.len().saturating_sub(1);
1075    let overlapped: Vec<usize> =
1076        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1077    let circle = match (rows.revolution_axis, rows.center.as_ref()) {
1078        (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
1079        (None, _) => Err("the rows are not an exact revolution".to_string()),
1080        (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
1081    };
1082    let (origin, axis, centre_radius, centre_height) = match circle {
1083        Ok((origin, axis, point)) => {
1084            let offset = point.sub(origin);
1085            let height = offset.dot(axis);
1086            (origin, axis, offset.sub(axis.scale(height)).length(), height)
1087        }
1088        Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
1089    };
1090    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1091        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1092    };
1093    overlapped
1094        .into_iter()
1095        .map(|interval| -> Result<f64, String> {
1096            let (from, to) = (parameters[interval], parameters[interval + 1]);
1097            let inside = |u: f64| u >= window[0] && u <= window[1];
1098            let mut probes: Vec<f64> =
1099                (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
1100            probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
1101            if probes.is_empty() {
1102                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1103            }
1104            let mut worst = 0.0_f64;
1105            for u in probes {
1106                for k in 1..OPEN_WALL_SECTIONS {
1107                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1108                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1109                    let offset = point.sub(origin);
1110                    let height = offset.dot(axis);
1111                    let radial = offset.sub(axis.scale(height)).length();
1112                    let standoff = (radial - centre_radius).hypot(height - centre_height);
1113                    let reading = (standoff - radius).abs();
1114                    if !reading.is_finite() {
1115                        return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
1116                    }
1117                    worst = worst.max(reading);
1118                }
1119            }
1120            Ok(worst)
1121        })
1122        .collect()
1123}
1124
1125pub(in crate::blend) fn open_wall_readings_why(
1126    rows: &FittedRows,
1127    parameters: &[f64],
1128    stations: &[Station],
1129    window: [f64; 2],
1130    dense: &OpenDenseCentres,
1131    radius: f64,
1132) -> Vec<Result<f64, String>> {
1133    let intervals = parameters.len().saturating_sub(1);
1134    let overlapped: Vec<usize> =
1135        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1136    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1137        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1138    };
1139    // The exact centres of one interval at j/32, its stations at both ends.
1140    let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
1141        let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
1142        if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
1143            return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
1144        }
1145        let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
1146        centres.push(stations[interval].center);
1147        for (j, centre) in inner.iter().enumerate() {
1148            centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
1149        }
1150        centres.push(stations[interval + 1].center);
1151        Ok(centres)
1152    };
1153    let half = OPEN_WALL_CENTRES / 2;
1154    // The junction stencil across the station between `left` and `left + 1`:
1155    // left's nodes half..=32, then right's nodes 1..=half; the station is node
1156    // `half`. With each node's NATIVE abscissa (the edge parameter its exact
1157    // centre was solved at, `OpenDenseCentres::station_ts`), and `None` when
1158    // the two intervals' native widths are bitwise equal: the nodes are then
1159    // uniform and the ordinary reader reads them, bit for bit.
1160    let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
1161        if dense.station_ts.len() != parameters.len() {
1162            return Err(format!("interval {left}: junction without the native station parameters"));
1163        }
1164        junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
1165    };
1166    overlapped
1167        .into_iter()
1168        .map(|interval| -> Result<f64, String> {
1169            let centres = nodes_of(interval)?;
1170            let (from, to) = (parameters[interval], parameters[interval + 1]);
1171            let inside = |u: f64| u >= window[0] && u <= window[1];
1172            // (u, stencil node, the stencil: None = this interval's own;
1173            // a junction's nodes with its native abscissae when unequal).
1174            let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
1175                .step_by(2)
1176                .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
1177                .filter(|(u, _)| inside(*u))
1178                .map(|(u, step)| (u, step, None))
1179                .collect();
1180            for end in window {
1181                if end > from && end < to {
1182                    let fraction = (end - from) / (to - from);
1183                    let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
1184                    #[cfg(not(test))]
1185                    let junction_off = false;
1186                    if junction_off {
1187                        probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1188                    } else if nearest < 2 && interval > 0 {
1189                        probes.push((end, half, Some(junction(interval - 1)?)));
1190                    } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
1191                        probes.push((end, half, Some(junction(interval)?)));
1192                    } else {
1193                        probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1194                    }
1195                }
1196            }
1197            if probes.is_empty() {
1198                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1199            }
1200            let mut declared = 0.0_f64;
1201            for (u, step, stencil) in probes {
1202                let (nodes, abscissae, what) = match &stencil {
1203                    Some((nodes, None)) => (nodes, None, "junction"),
1204                    Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
1205                    None => (&centres, None, "interval"),
1206                };
1207                for k in 1..OPEN_WALL_SECTIONS {
1208                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1209                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1210                    // The interval's own stencil reads from the probe's node,
1211                    // then (declined) from where its fraction reaches along the
1212                    // centre path (a stationary section); a junction keeps its
1213                    // own dispatch.
1214                    let read = if stencil.is_none() {
1215                        crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
1216                    } else {
1217                        read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
1218                    };
1219                    let (reading, uncertainty) = read.map_err(|why| {
1220                        format!(
1221                            "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
1222                             (u {u:.17}): {why}"
1223                        )
1224                    })?;
1225                    declared = declared.max(reading + uncertainty);
1226                }
1227            }
1228            if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
1229        })
1230        .collect()
1231}
1232
1233
1234/// A fresh id allocator seeded past the vertex, edge, face, loop and coedge IDs.
1235/// Solid and shell IDs are outside this allocation domain. Shared by
1236/// a whole group of stripes so their vertices, edges and coedges cannot
1237/// collide.
1238pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
1239    let mut next_id = solid
1240        .vertices
1241        .iter()
1242        .map(|vertex| vertex.id)
1243        .chain(solid.edges.iter().map(|edge| edge.id))
1244        .chain(
1245            solid
1246                .shells
1247                .iter()
1248                .flat_map(|shell| &shell.faces)
1249                .flat_map(|face| {
1250                    face.loops
1251                        .iter()
1252                        .map(|loop_record| loop_record.id)
1253                        .chain(face.loops.iter().flat_map(|loop_record| {
1254                            loop_record.coedges.iter().map(|coedge| coedge.id)
1255                        }))
1256                        .chain(std::iter::once(face.id))
1257                }),
1258        )
1259        .max()
1260        .unwrap_or(0)
1261        + 1;
1262    move || {
1263        let id = next_id;
1264        next_id += 1;
1265        id
1266    }
1267}
1268
1269
1270/// May a declined support refit fall back on the march's own trim pcurve?
1271/// Only when that trim, read independently as shipped
1272/// (`read_shipped_pcurve`: miss, rail standoff, samples, worst), meets the
1273/// floor ON its rail's branch: its miss PLUS the rail's standoff from the
1274/// carrier at the trim's foot within `floor`. A trim on another branch of the
1275/// carrier tracks its own foot perfectly (miss 0) while that foot stands far
1276/// off the rail, so a miss alone would ship it; an unreadable trim never
1277/// stands.
1278pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
1279    matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
1280}
1281
1282/// Slice 1b of the surgery lane (`exact_trim_on`): the support trim of a
1283/// straight rail on a PLANE, exact. The feet of the rail's two ends on the
1284/// plane (the kernel's closed-form projection) joined by a degree-1 pcurve
1285/// on the rail's own parameter range, so the surgery reads it at the rail's
1286/// parameter as it reads every trim; read once as shipped
1287/// (`read_shipped_pcurve`) and kept only when it stands within the pcurve
1288/// floor by the march trim's own rule (`march_trim_stands`: miss plus rail
1289/// standoff). `None` hands the trim to the refit road, as a march trim that
1290/// does not stand would be.
1291fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
1292    if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
1293        return Ok(None);
1294    }
1295    let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1296    let mut control = Vec::with_capacity(2);
1297    for t in [d0, d1] {
1298        let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1299        let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1300        control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1301    }
1302    let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1303    feet_timing.stop();
1304    let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1305    let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
1306        Some(read) => {
1307            crate::blend::record_count("count.read.affine_trims", 1);
1308            Ok(read)
1309        }
1310        None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
1311    };
1312    read_timing.stop();
1313    Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1314}
1315
1316/// [`read_shipped_pcurve`](crate::blend::track_fit::read_shipped_pcurve)'s
1317/// `(miss, standoff, samples, worst)` for a degree-1 `pcurve` on an AFFINE
1318/// plane against an affine rail, read at `d0` and `d1` only: the pcurve's
1319/// image, the rail and the rail's foot on the plane are all affine in the
1320/// rail parameter, so the miss (image to foot) and the standoff (rail to foot)
1321/// are norms of affine functions, convex, and largest at an end. `None` when
1322/// any of the three is not affine, or an end will not evaluate.
1323fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
1324    if !affine_curve(rail) || !affine_curve(pcurve) {
1325        return None;
1326    }
1327    let (origin, normal) = affine_plane(surface)?;
1328    let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
1329    for t in [d0, d1] {
1330        let point = rail.evaluate(t).ok()?;
1331        let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
1332        let uv = pcurve.evaluate(t).ok()?;
1333        let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
1334        let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
1335        if !(residual.is_finite() && off.is_finite()) {
1336            return None;
1337        }
1338        if residual > miss {
1339            (miss, worst) = (residual, t);
1340        }
1341        standoff = standoff.max(off);
1342    }
1343    Some((miss, standoff, 2, worst))
1344}
1345
1346/// Sew ONE stripe into `result`.
1347///
1348/// `solid` is the ORIGINAL topology every stripe was marched against (the
1349/// row-coincidence detection reads it); `result` is the shared evolving
1350/// solid, and `take_id` the shared id allocator — a group of stripes meeting
1351/// at a corner must create their rim vertices and cross-section arcs from one
1352/// counter, and must see each other's work.
1353///
1354/// Each end is either free (terminate on the face across the corner) or a
1355/// corner stop; see [`EndPlan`].  Pruning orphaned vertices is the caller's
1356/// job, once, after every stripe of the group is in.
1357pub(in crate::blend) fn build_open_surgery(
1358    solid: &BrepSolid,
1359    result: &mut BrepSolid,
1360    take_id: &mut dyn FnMut() -> u64,
1361    edge: &EdgeRecord,
1362    first: &BlendMate,
1363    second: &BlendMate,
1364    rows: FittedRows,
1365    ends: [EndPlan; 2],
1366    name: Option<&str>,
1367) -> Result<SewnStripe, KernelRefusal> {
1368    build_open_surgery_with_tail(solid, result, take_id, edge, first, second, rows, ends, name, [None, None])
1369}
1370
1371/// [`build_open_surgery`] for a stripe whose finish end rolled onto tangent
1372/// neighbours (`crate::blend::rollover::stopped_end_tail`, switch, default
1373/// off): after the pristine sewing, each rolled side's rail is split at its
1374/// crossing vertex, the part past it and the cap's leg move from the mate's
1375/// loop into the neighbour's — where they replace the run of the crossed
1376/// tangent edge from the crossing to the sharp vertex, which is deleted —
1377/// and the part past it gets its pcurve on the neighbour. The mate keeps the
1378/// rail up to the crossing and the rest of the tangent edge. With no rolled
1379/// side the sewing is exactly [`build_open_surgery`]'s.
1380#[allow(clippy::too_many_arguments)]
1381pub(in crate::blend) fn build_open_surgery_with_tail(
1382    solid: &BrepSolid,
1383    result: &mut BrepSolid,
1384    take_id: &mut dyn FnMut() -> u64,
1385    edge: &EdgeRecord,
1386    first: &BlendMate,
1387    second: &BlendMate,
1388    rows: FittedRows,
1389    ends: [EndPlan; 2],
1390    name: Option<&str>,
1391    tail: [Option<crate::blend::rollover::TailSplice<'_>>; 2],
1392) -> Result<SewnStripe, KernelRefusal> {
1393    let [start_end, finish_end] = ends;
1394
1395    // Trim the support rows to the crossing window.
1396    // A POLE end stops EXACTLY on the row's own end (the march puts the
1397    // pole's station there and overshoots no further), so there is nothing to
1398    // split off on that side.  Only equality: a stop PAST the row's end — a
1399    // free end's crossing the rows never reached — still fails the split and
1400    // refuses, rather than building a rail out to wherever the rows stop.
1401    let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1402        let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1403        let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1404        if b == high {
1405            return Ok(tail);
1406        }
1407        let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1408        Ok(middle)
1409    };
1410    let kind = |plan: &EndPlan| match plan {
1411        EndPlan::Free(_) => "free",
1412        EndPlan::Corner(_) => "corner",
1413        EndPlan::Miter(_) => "miter",
1414        EndPlan::Cap(_) => "cap",
1415    };
1416    let describe_trim = |error: KernelRefusal| {
1417        error.with_message(|error| {
1418            format!(
1419                "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1420                 at cr {:.6}/cs {:.6})",
1421                edge.id,
1422                kind(&start_end),
1423                start_end.cr_parameter(),
1424                start_end.cs_parameter(),
1425                kind(&finish_end),
1426                finish_end.cr_parameter(),
1427                finish_end.cs_parameter()
1428            )
1429        })
1430    };
1431    // A rail whose two stops are one point within `consumed_band` has no strip
1432    // to trim: the blend takes that mate down to a single point along this
1433    // edge.  That is a FULL-WIDTH corner — the shared face of a miter whose
1434    // radius is the side face's width, every face of a star at full width, the
1435    // middle edge of a channel whose two corner setbacks meet.  No rail edge is
1436    // built; the mate loses the blended edge instead of having it replaced, and
1437    // the rail's two rim vertices are one vertex, which the orchestrator
1438    // identifies (`SewnStripe::identified`) once every stripe is in.  A stripe
1439    // free at BOTH ends has no corner to collapse onto, so it refuses.
1440    let band = consumed_band(solid);
1441    let exact_trim = crate::blend::exact_trim_on();
1442    let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1443        let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1444        Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1445            && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1446    };
1447    let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1448    let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1449    if cr_collapsed || cs_collapsed {
1450        let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1451            Some("neither end is a corner")
1452        } else if first.face.id == second.face.id {
1453            Some("both rails lie on one face")
1454        } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1455            Some("a capped end has no corner to share the point with")
1456        } else {
1457            None
1458        };
1459        if let Some(reason) = unsupported {
1460            return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1461                "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1462                edge.id
1463            )));
1464        }
1465    }
1466    // The support pcurve the rows carry interpolates the stations' (u, v) and
1467    // is read nowhere between them: on the 20-degree crossing's notched open
1468    // exit arc it stood 2.98e-5 off the rail it trims (q1 = 9cd8c3163 + the
1469    // section-fit fix, read against the exact cylinders). Inside the crossing
1470    // window — on the face, never on a carrier's extension — it is refitted
1471    // to the trimmed rail's own projected track at the pcurve floor and read
1472    // again as shipped (`refit_closed_support_pcurve`, the closed edge's).
1473    // A rail that overruns a PLANE carrier's chart (the rib-base fin top's
1474    // rails run 0.303 past it) is on the plane's extension, which the chart
1475    // clamps: there the refit and both reads would measure the clamp, and the
1476    // trim is instead rebuilt from its edge on the widened chart and verified
1477    // at the floor by `fit_planar_charts_to_trims`, which every lane that
1478    // sews these stripes runs.
1479    let trim_rail = |collapsed: bool,
1480                     row: &NurbsCurve,
1481                     pcurve: &NurbsCurve,
1482                     surface: &NurbsSurface,
1483                     a: f64,
1484                     b: f64|
1485     -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1486        if collapsed {
1487            return Ok(None);
1488        }
1489        let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1490        let rail = trim_row(row, a, b).map_err(describe_trim)?;
1491        let interpolated = trim_row(pcurve, a, b)?;
1492        rows_timing.stop();
1493        {
1494            let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1495            let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1496            let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1497            overrun_timing.stop();
1498            if overrun.is_some_and(|excursion| excursion > band) {
1499                return Ok(Some((rail, interpolated)));
1500            }
1501        }
1502        let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1503        // Slice 1b (`exact_trim_on`): an exact extrusion row's rail on a
1504        // plane ships the exact line in uv, read once; the refit road below
1505        // is for what it declines.
1506        if exact_trim && rows.exact_extrusion {
1507            if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1508                crate::blend::record_count("count.surgery.exact_trim", 1);
1509                return Ok(Some((rail, line)));
1510            }
1511        }
1512        // The exact REVOLUTION rows' own trims are exact by construction
1513        // (the carrier's uv line, the plane's affine image of the rail):
1514        // read once as shipped by the march trim's rule (`march_trim_stands`)
1515        // and kept; whatever does not stand takes the refit road below.
1516        if rows.revolution_axis.is_some() {
1517            // Lane N row 4: the pair bounded in closed form stands without a
1518            // sample (`exact_revolution_read_bound` on the UNCUT row and
1519            // pcurve, whose restrictions to [a, b] these are: miss + standoff
1520            // within three bounds).
1521            if exact_revolution_reads_on() {
1522                let cert_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.cert_ms");
1523                let bound = exact_revolution_read_bound(row, surface, pcurve);
1524                cert_timing.stop();
1525                if bound.is_some_and(|bound| 3.0 * bound <= crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1526                    crate::blend::record_count("count.surgery.exact_revolution_trim_certified", 1);
1527                    return Ok(Some((rail, interpolated)));
1528                }
1529            }
1530            let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1531            let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1532            read_timing.stop();
1533            if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1534                crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1535                return Ok(Some((rail, interpolated)));
1536            }
1537            crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1538        }
1539        // The march's OWN trim pcurve, read independently at the rail's own
1540        // parameter against the same floor (`read_shipped_pcurve`): what a
1541        // declined refit may fall back to, and the standoff a refit on the
1542        // rail's own branch must match.
1543        // The feet of each pcurve against the rail are sought once and
1544        // shared between the reads below (`FootMemo`): the march trim's for
1545        // its own read and the branch comparison, the refit's for the
1546        // refit's own read, the branch comparison and the absolute bound.
1547        let mut march_feet = crate::blend::track_fit::FootMemo::default();
1548        let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1549        let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1550        let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1551        march_read_timing.stop();
1552        let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1553        let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1554            // Kept ONLY when the march's trim stands on its own read
1555            // (`march_trim_stands`); otherwise the refusal stands.
1556            let stands = march_trim_stands(&march_read, floor);
1557            if stands {
1558                Ok(Some((rail.clone(), interpolated.clone())))
1559            } else {
1560                Err(why)
1561            }
1562        };
1563        let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1564        let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1565        refit_timing.stop();
1566        let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1567        match refit {
1568            Ok((fitted, refit_read)) => {
1569                // A refit tracks ITS OWN projected track; a track that took
1570                // another branch of the carrier tracks itself perfectly while
1571                // its foot stands far off the rail (the stationary-start
1572                // prism's coedge read 6.0 off its edge). Its foot standoff
1573                // must be the rail's own standoff on the march trim's branch.
1574                // BOTH reads are required: an unreadable refit read declines
1575                // the refit (the march trim then stands only on its own read
1576                // within the floor), and an unreadable march read cannot
1577                // establish that the refit is on the rail's branch, so it
1578                // refuses. Never shipped on an unknown.
1579                // The two are compared POINTWISE, at the same rail
1580                // parameters (`standoff_excess`), never as two maxima read at
1581                // each curve's own knots.
1582                // The refit's read is the shipped curve's own, handed back by
1583                // the refit (the same `read_shipped_pcurve` on the same
1584                // inputs, once).
1585                let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1586                    (_, Err(error)) => return declined(error),
1587                    (Err(error), Ok(_)) => {
1588                        return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1589                            "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1590                             trim is unreadable ({})",
1591                            error.message
1592                        )));
1593                    }
1594                    (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1595                };
1596                let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1597                let excess = crate::blend::track_fit::standoff_excess_memo(
1598                    surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1599                );
1600                excess_timing.stop();
1601                let excess = match excess {
1602                    Ok(excess) => excess,
1603                    Err(error) => return declined(error),
1604                };
1605                if !(excess <= floor) {
1606                    return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1607                        "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1608                         at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1609                         another branch of the carrier"
1610                    )));
1611                }
1612                // ABSOLUTE: the comparison above is relative to the march
1613                // trim, so a refit that follows an off-branch march trim
1614                // passes it. The refit's foot must also stand within the floor
1615                // of the rail's own deviation from its carrier, read by the
1616                // nearest-point projector at the same parameters
1617                // (`standoff_over_nearest`) -- the rail's approximation the
1618                // lane reports, never more.
1619                let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1620                let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1621                nearest_timing.stop();
1622                let absolute = match absolute {
1623                    Ok(absolute) => absolute,
1624                    Err(error) => return declined(error),
1625                };
1626                if !(absolute <= floor) {
1627                    return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1628                        "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1629                         off its carrier: its foot is not the rail's nearest point"
1630                    )));
1631                }
1632                Ok(Some((rail, fitted)))
1633            }
1634            Err(error) => declined(error),
1635        }
1636    };
1637    let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1638    let cr_rail = trim_rail(
1639        cr_collapsed,
1640        &rows.cr,
1641        &rows.cr_pcurve,
1642        &first.face.surface,
1643        start_end.cr_parameter(),
1644        finish_end.cr_parameter(),
1645    )?;
1646    let cs_rail = trim_rail(
1647        cs_collapsed,
1648        &rows.cs,
1649        &rows.cs_pcurve,
1650        &second.face.surface,
1651        start_end.cs_parameter(),
1652        finish_end.cs_parameter(),
1653    )?;
1654    trim_timing.stop();
1655    // A collapsed rail's domain is its two stops, which are one point.
1656    let cr_domain = match &cr_rail {
1657        Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1658        None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1659    };
1660    let cs_domain = match &cs_rail {
1661        Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1662        None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1663    };
1664    // A rail's end points, read off the trimmed rail or, collapsed, the row.
1665    let cr_at = |parameter: f64| match &cr_rail {
1666        Some((cr, _)) => cr.evaluate(parameter),
1667        None => rows.cr.evaluate(parameter),
1668    };
1669    let cs_at = |parameter: f64| match &cs_rail {
1670        Some((cs, _)) => cs.evaluate(parameter),
1671        None => rows.cs.evaluate(parameter),
1672    };
1673    let old_start = edge.start_vertex_id;
1674    let old_finish = edge.end_vertex_id;
1675
1676    // Classify each of the four support crossings.  A crossing that lands at
1677    // the FAR endpoint of the boundary edge it meets (the end away from the
1678    // corner) means a prior fillet already consumed that whole boundary: the
1679    // new blend reuses the existing vertex there and the boundary edge is
1680    // deleted (its role is taken over by the new transverse curve on the
1681    // prior blend face).  An interior crossing is the pristine case — a fresh
1682    // vertex with the boundary trimmed to it.
1683    //
1684    // Consumed means within `consumed_band` — the slack `check_support_extent`
1685    // refuses past, so a rail between the two is not possible.
1686    let classify =
1687        |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1688            let boundary = result
1689                .edges
1690                .iter()
1691                .find(|candidate| candidate.id == boundary_id)
1692                .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1693            let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1694            let far_vertex = if boundary.start_vertex_id == corner {
1695                Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1696            } else if boundary.end_vertex_id == corner {
1697                Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1698            } else {
1699                None
1700            };
1701            if let Some((far_id, far_t, near_t)) = far_vertex {
1702                // A consumed boundary is a COINCIDENCE — a prior fillet's rim
1703                // vertex is exactly where this rail crosses — and the operands
1704                // were healed before the march, so it holds to solver
1705                // precision.  It is not "near the far end": a fillet whose
1706                // radius is 1e-3 short of the face's width crosses 1e-3 from
1707                // the far vertex and must keep that sliver, or the face is
1708                // snapped a full 1e-3 out of true (the oversized-radius limit
1709                // cases and issue 1177 measure exactly that).  Measured in
1710                // model units, never in the boundary's parameter — a unit
1711                // parameter on a 20-long edge would call 2e-3 a coincidence.
1712                let far_point = result
1713                    .vertices
1714                    .iter()
1715                    .find(|candidate| candidate.id == far_id)
1716                    .map(|candidate| candidate.point)
1717                    .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1718                let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1719                let _ = span;
1720                let past_far = crossing.sub(far_point).length();
1721                if past_far <= band {
1722                    // Sanity: the reused vertex must exist.
1723                    if !result
1724                        .vertices
1725                        .iter()
1726                        .any(|candidate| candidate.id == far_id)
1727                    {
1728                        return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1729                    }
1730                    return Ok(RimResolution::Consumed(far_id));
1731                }
1732                // A crossing BEYOND the far vertex is a rail that runs off
1733                // its face: the blend is wider than the face by more than the
1734                // band a snap may close, and trimming the boundary there would
1735                // extend it into air.  `check_support_extent` refuses this
1736                // before the march on every face the exact tool understands;
1737                // this is the refusal for the rest.  It is not the whole
1738                // answer: a crossing the solve CLAMPS to the far vertex reads
1739                // as consumed here, and `detect_row_coincidence` is what sees
1740                // that row leave the face.  Measured on the census: it fires on
1741                // a wall wider than its closed chain's arc (0.5 past) and on the
1742                // r = 10.001 pinch past an earlier round (2e-3 past), both of
1743                // which refused later in the surgery before.
1744                if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1745                    return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1746                        "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1747                         past that edge's far vertex, and a rail within {band:.3e} of it is the \
1748                         most this surgery snaps onto the edge"
1749                    )));
1750                }
1751            }
1752            Ok(RimResolution::Fresh)
1753        };
1754    // A CORNER end has no boundary to classify: the ball's tangency vertex is
1755    // the rim, and the boundary edge that used to reach the sharp corner is
1756    // the neighbouring stripe's own blended edge, which that stripe replaces.
1757    let resolve_side = |plan: &EndPlan,
1758                        side_first: bool,
1759                        corner: u64|
1760     -> Result<RimResolution, KernelRefusal> {
1761        if let Some(vertex) = plan.planned_rim(side_first) {
1762            return Ok(RimResolution::Corner(vertex));
1763        }
1764        match plan {
1765            EndPlan::Free(free) => classify(
1766                if side_first {
1767                    free.first_edge_id
1768                } else {
1769                    free.second_edge_id
1770                },
1771                if side_first {
1772                    free.first_edge_parameter
1773                } else {
1774                    free.second_edge_parameter
1775                },
1776                corner,
1777            ),
1778            EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1779                Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1780            }
1781        }
1782    };
1783    let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1784    let start_first = resolve_side(&start_end, true, old_start)?;
1785    let start_second = resolve_side(&start_end, false, old_start)?;
1786    let finish_first = resolve_side(&finish_end, true, old_finish)?;
1787    let finish_second = resolve_side(&finish_end, false, old_finish)?;
1788    crossings_timing.stop();
1789    let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1790
1791    // Manifold pairing: the blend's use of the first support row must oppose
1792    // F1's use of the blended edge (see the blend-loop construction below).
1793    let first_use_forward = first.coedge.forward;
1794    let blend_cr_forward = !first_use_forward;
1795
1796    // Curve-level coincidence (OCCT PR #1449 lesson 3): when BOTH of a mate's
1797    // crossings are endpoint-consumed AND the trimmed support row retraces the
1798    // existing edge joining the two far vertices, that mate face is FULLY
1799    // consumed — the surgery sews the blend face straight onto the existing
1800    // edge and drops the zero-width face, instead of creating a coincident
1801    // fresh support edge that would leave a sliver strip (the r = face-width
1802    // class).  Each side is detected independently; any ambiguity keeps the
1803    // pristine path (fail-safe).  `row_traversed_from_start`: the blend loop
1804    // walks cr in station order iff `blend_cr_forward`, and cs in the
1805    // OPPOSITE order (see the two loop branches below).
1806    // A stripe with a CORNER end has no boundary edges at that end to be
1807    // coincident with, so the detection only runs on stripes that are free at
1808    // both ends.  (Fail-safe: not detecting a coincidence keeps the pristine
1809    // path, which is what a corner stop wants anyway.)
1810    let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1811        (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1812            detect_row_coincidence(
1813                solid,
1814                first,
1815                edge.id,
1816                start_free.first_edge_id,
1817                finish_free.first_edge_id,
1818                start_first.is_consumed(),
1819                start_first.existing().unwrap_or(0),
1820                finish_first.is_consumed(),
1821                finish_first.existing().unwrap_or(0),
1822                cr,
1823                blend_cr_forward,
1824                band,
1825            )?,
1826            detect_row_coincidence(
1827                solid,
1828                second,
1829                edge.id,
1830                start_free.second_edge_id,
1831                finish_free.second_edge_id,
1832                start_second.is_consumed(),
1833                start_second.existing().unwrap_or(0),
1834                finish_second.is_consumed(),
1835                finish_second.existing().unwrap_or(0),
1836                cs,
1837                !blend_cr_forward,
1838                band,
1839            )?,
1840        ),
1841        _ => (None, None),
1842    };
1843    // One face hosting both sides (the blended edge used twice by one face)
1844    // cannot be dropped for one side while the other still splices into it —
1845    // ambiguous, keep the pristine path for both.
1846    let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1847        if first.face.id == second.face.id {
1848            (None, None)
1849        } else {
1850            (sew_first, sew_second)
1851        };
1852
1853    // Resolve the four rim vertices: reuse the existing vertex for a consumed
1854    // side, else create a fresh vertex at the support-row endpoint.
1855    let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1856        match rim.existing() {
1857            Some(id) => Ok(id),
1858            None => {
1859                let id = take_id();
1860                result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1861                Ok(id)
1862            }
1863        }
1864    };
1865    let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1866    let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1867    let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1868    let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1869    // How far the consumed rims and sewn rows moved a rail onto what already
1870    // existed — what `check_snap_closure` measures the body against.
1871    let mut snap = [&sew_first, &sew_second]
1872        .into_iter()
1873        .flatten()
1874        .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1875    for (rim, vertex, row_end) in [
1876        (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1877        (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1878        (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1879        (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1880    ] {
1881        if !rim.is_consumed() {
1882            continue;
1883        }
1884        let point = result
1885            .vertices
1886            .iter()
1887            .find(|candidate| candidate.id == vertex)
1888            .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1889            .point;
1890        snap = snap.max(row_end.sub(point).length());
1891    }
1892
1893    // Support edges: a sewn side reuses the EXISTING coincident edge (no
1894    // fresh edge — OCCT's `SetExistingEdge` move); a pristine side gets the
1895    // fitted row as a fresh edge.
1896    // A collapsed side builds no edge at all.
1897    let cr_edge_id = match (&sew_first, &cr_rail) {
1898        (Some(sew), _) => Some(sew.edge_id),
1899        (None, None) => None,
1900        (None, Some((cr, _))) => {
1901            let id = take_id();
1902            result.edges.push(EdgeRecord {
1903                id,
1904                curve: cr.clone(),
1905                t0: cr_domain[0],
1906                t1: cr_domain[1],
1907                start_vertex_id: w1a,
1908                end_vertex_id: w1b,
1909                degenerate: false,
1910                name: None,
1911            });
1912            Some(id)
1913        }
1914    };
1915    let cs_edge_id = match (&sew_second, &cs_rail) {
1916        (Some(sew), _) => Some(sew.edge_id),
1917        (None, None) => None,
1918        (None, Some((cs, _))) => {
1919            let id = take_id();
1920            result.edges.push(EdgeRecord {
1921                id,
1922                curve: cs.clone(),
1923                t0: cs_domain[0],
1924                t1: cs_domain[1],
1925                start_vertex_id: w2a,
1926                end_vertex_id: w2b,
1927                degenerate: false,
1928                name: None,
1929            });
1930            Some(id)
1931        }
1932    };
1933    // The edge closing each end of the blend face: the §6.9 transverse curve
1934    // on the end face for a free end, or — at a corner — the end
1935    // cross-section arc the corner patch was already given, committed by the
1936    // caller and merely referenced here.
1937    let mut commit_split = |plan: &EndPlan, second_rim: u64, corner: u64| -> Result<Option<super::split_floor::SplitCommit>, KernelRefusal> {
1938        match plan.free().and_then(|free| free.split.as_ref()) {
1939            Some(split) => super::split_floor::commit_split_end(result, &mut *take_id, split, second_rim, corner).map(Some),
1940            None => Ok(None),
1941        }
1942    };
1943    let start_split = commit_split(&start_end, w2a, old_start)?;
1944    let finish_split = commit_split(&finish_end, w2b, old_finish)?;
1945    let mut commit_end_edge = |plan: &EndPlan,
1946                               first_rim: u64,
1947                               second_rim: u64|
1948     -> Result<Option<u64>, KernelRefusal> {
1949        match plan {
1950            EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1951            EndPlan::Free(free) => {
1952                let id = take_id();
1953                let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1954                result.edges.push(EdgeRecord {
1955                    id,
1956                    curve: free.transverse_curve.clone(),
1957                    t0: domain[0],
1958                    t1: domain[1],
1959                    start_vertex_id: first_rim,
1960                    end_vertex_id: second_rim,
1961                    degenerate: false,
1962                    name: None,
1963                });
1964                Ok(Some(id))
1965            }
1966        }
1967    };
1968    let transverse_a_id = commit_end_edge(&start_end, w1a, start_split.as_ref().map_or(w2a, |split| split.q1_vertex))?;
1969    let transverse_b_id = commit_end_edge(&finish_end, w1b, finish_split.as_ref().map_or(w2b, |split| split.q1_vertex))?;
1970
1971    // Replace the blended edge in each PRISTINE mate's loop — the blend
1972    // face's loop direction is forced by manifold pairing with F1's use of
1973    // the blended edge (`first_use_forward`, captured above).  A sewn mate is
1974    // dropped whole below; nothing to splice.  A COLLAPSED side has no rail to
1975    // put there: the mate simply loses the blended edge, and its loop closes
1976    // through the rim vertices the orchestrator identifies.
1977    for (mate, rail, sewn) in [
1978        (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1979        (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1980    ] {
1981        if sewn {
1982            continue;
1983        }
1984        let face = result
1985            .shells
1986            .iter_mut()
1987            .flat_map(|shell| &mut shell.faces)
1988            .find(|face| face.id == mate.face.id)
1989            .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1990        let loop_record = &mut face.loops[mate.loop_index];
1991        let position = loop_record
1992            .coedges
1993            .iter()
1994            .position(|coedge| coedge.edge_id == edge.id)
1995            .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1996        let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1997            loop_record.coedges.remove(position);
1998            continue;
1999        };
2000        let old_forward = loop_record.coedges[position].forward;
2001        loop_record.coedges[position] = CoedgeRecord {
2002            id: loop_record.coedges[position].id,
2003            edge_id: new_edge_id,
2004            forward: old_forward,
2005            pcurve: if old_forward {
2006                pcurve_forward.clone()
2007            } else {
2008                pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
2009            },
2010        };
2011    }
2012
2013    // A capped end's legs: on each mate, the leg runs from the rail's rim
2014    // vertex to the sharp vertex, where the continuing edge still starts.
2015    // It goes between the rail coedge and that continuing coedge, with the
2016    // sense that walks rim -> vertex when the rail ends at the rim.
2017    for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
2018        let EndPlan::Cap(cap) = plan else {
2019            continue;
2020        };
2021        for (mate, rail_edge_id, leg, rim) in [
2022            (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
2023            (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
2024        ] {
2025            // Refused above: a capped end never meets a collapsed rail.
2026            let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
2027            let face = result
2028                .shells
2029                .iter_mut()
2030                .flat_map(|shell| &mut shell.faces)
2031                .find(|face| face.id == mate.face.id)
2032                .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
2033            let loop_record = &mut face.loops[mate.loop_index];
2034            let count = loop_record.coedges.len();
2035            let rail_at = loop_record
2036                .coedges
2037                .iter()
2038                .position(|coedge| coedge.edge_id == rail_edge_id)
2039                .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
2040            // Which side of the rail coedge is this end?  The rail coedge
2041            // traverses rim-to-rim; the capped end is at its traversal END
2042            // when walking forward means station order and this is the
2043            // finish end, etc.  Decide by geometry: the neighbour whose
2044            // traversal touches the sharp vertex.
2045            let rail_forward = loop_record.coedges[rail_at].forward;
2046            let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
2047            let (insert_at, forward, pcurve) = if end_is_traversal_end {
2048                // rail ... rim -> [leg rim->vertex] -> continuing edge
2049                (rail_at + 1, true, leg.1.clone())
2050            } else {
2051                // continuing edge -> [leg vertex->rim] -> rim ... rail
2052                (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
2053            };
2054            let _ = (count, rim);
2055            loop_record.coedges.insert(
2056                insert_at,
2057                CoedgeRecord {
2058                    id: take_id(),
2059                    edge_id: leg.0,
2060                    forward,
2061                    pcurve,
2062                },
2063            );
2064        }
2065    }
2066
2067    // Drop consumed boundary edges from the mate loop they share with the
2068    // blended edge: the fresh support coedge already begins at the reused far
2069    // vertex, so removing the fully-covered boundary keeps the loop closed.
2070    // A SEWN mate's whole loop collapses (the face is dropped below), so its
2071    // consumed boundaries are only recorded for deletion, never spliced.
2072    let mut consumed_edges: Vec<u64> = Vec::new();
2073    let start_free = start_end.free();
2074    let finish_free = finish_end.free();
2075    for (mate, sewn, sides) in [
2076        (
2077            first,
2078            sew_first.is_some(),
2079            [
2080                (&start_first, start_free.map(|free| free.first_edge_id)),
2081                (&finish_first, finish_free.map(|free| free.first_edge_id)),
2082            ],
2083        ),
2084        (
2085            second,
2086            sew_second.is_some(),
2087            [
2088                (&start_second, start_free.map(|free| free.second_edge_id)),
2089                (&finish_second, finish_free.map(|free| free.second_edge_id)),
2090            ],
2091        ),
2092    ] {
2093        for (cross, boundary_id) in sides {
2094            if !cross.is_consumed() {
2095                continue;
2096            }
2097            let Some(boundary_id) = boundary_id else {
2098                continue;
2099            };
2100            if !sewn {
2101                let face = result
2102                    .shells
2103                    .iter_mut()
2104                    .flat_map(|shell| &mut shell.faces)
2105                    .find(|face| face.id == mate.face.id)
2106                    .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2107                face.loops[mate.loop_index]
2108                    .coedges
2109                    .retain(|coedge| coedge.edge_id != boundary_id);
2110            }
2111            if !consumed_edges.contains(&boundary_id) {
2112                consumed_edges.push(boundary_id);
2113            }
2114        }
2115    }
2116    // Lesson 7: zero-span leftovers of a collapsing loop go with their face.
2117    for sew in [&sew_first, &sew_second].into_iter().flatten() {
2118        for id in &sew.collapsed_edges {
2119            if !consumed_edges.contains(id) {
2120                consumed_edges.push(*id);
2121            }
2122        }
2123    }
2124
2125    // Locate the corner junction on each end face BEFORE trimming (the two
2126    // boundary coedges that meet at the old corner vertex, stepping over the
2127    // pole between them where the end face's apex IS that vertex) — edge ids
2128    // alone are ambiguous on two-coedge cap loops, so the meeting must be at
2129    // the corner.
2130    // (end, transverse_id, corner, first_consumed, second_consumed) -> plan.
2131    // Only FREE ends appear here: a corner stop rebuilds nothing on a third
2132    // face, because the corner patch is what closes it.
2133    let mut end_plans = Vec::with_capacity(2);
2134    for (end, transverse_id, corner, first_consumed, second_consumed) in [
2135        (
2136            start_end.free(),
2137            transverse_a_id,
2138            old_start,
2139            start_first.is_consumed(),
2140            start_second.is_consumed(),
2141        ),
2142        (
2143            finish_end.free(),
2144            transverse_b_id,
2145            old_finish,
2146            finish_first.is_consumed(),
2147            finish_second.is_consumed(),
2148        ),
2149    ] {
2150        let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
2151            continue;
2152        };
2153        if end.split.is_some() {
2154            continue;
2155        }
2156        let face = result
2157            .shells
2158            .iter()
2159            .flat_map(|shell| &shell.faces)
2160            .find(|face| face.id == end.end_face_id)
2161            .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
2162        // Key on stable coedge ids (not indices) so processing one end can't
2163        // invalidate another that shares this face.
2164        let Some(located) = locate_end_corner(
2165            &result,
2166            face,
2167            corner,
2168            end.first_edge_id,
2169            end.second_edge_id,
2170        ) else {
2171            return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
2172        };
2173        // The apex the transverse curve cuts off goes with the boundaries the
2174        // blend consumes.
2175        for pole in &located.pole_edge_ids {
2176            if !consumed_edges.contains(pole) {
2177                consumed_edges.push(*pole);
2178            }
2179        }
2180        end_plans.push((
2181            end.end_face_id,
2182            transverse_id,
2183            end.transverse_end_pcurve.clone(),
2184            located,
2185            first_consumed,
2186            second_consumed,
2187        ));
2188    }
2189
2190    let split_corners = |end: Option<&EndSurgery>, corner: u64, result: &BrepSolid| -> Result<Option<[super::support::EndCorner; 2]>, KernelRefusal> {
2191        match end.and_then(|end| end.split.as_ref().map(|split| (end, split))) {
2192            Some((end, split)) => super::split_floor::locate_split_corners(result, end, split, corner).map(Some),
2193            None => Ok(None),
2194        }
2195    };
2196    let start_split_corners = split_corners(start_free, old_start, result)?;
2197    let finish_split_corners = split_corners(finish_free, old_finish, result)?;
2198    if (start_split_corners.is_some() && (start_first.is_consumed() || start_second.is_consumed()))
2199        || (finish_split_corners.is_some() && (finish_first.is_consumed() || finish_second.is_consumed()))
2200    {
2201        return Err(KernelRefusal::unsupported(KernelStage::Sew, "split_floor_consumed", "blend: a two-face end whose rail consumes its boundary"));
2202    }
2203
2204    // Trim the boundary edges at their support crossings.  A consumed side is
2205    // deleted instead of trimmed; a CORNER side touches no boundary at all.
2206    for (rim_resolution, boundary, corner, rim) in [
2207        (
2208            &start_first,
2209            start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2210            old_start,
2211            w1a,
2212        ),
2213        (
2214            &start_second,
2215            start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2216            old_start,
2217            w2a,
2218        ),
2219        (
2220            &finish_first,
2221            finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2222            old_finish,
2223            w1b,
2224        ),
2225        (
2226            &finish_second,
2227            finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2228            old_finish,
2229            w2b,
2230        ),
2231    ] {
2232        if !rim_resolution.trims_boundary() {
2233            continue;
2234        }
2235        let Some((boundary_id, boundary_param)) = boundary else {
2236            continue;
2237        };
2238        trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
2239    }
2240
2241    // Rebuild each end face's corner: drop the consumed boundary coedge(s) and
2242    // splice in the transverse coedge between the (trimmed) boundaries.
2243    for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
2244        end_plans
2245    {
2246        let forward = located.x_is_first;
2247        let pcurve = if forward {
2248            transverse_end_pcurve
2249        } else {
2250            transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2251        };
2252        let (x_consumed, y_consumed) = if located.x_is_first {
2253            (first_consumed, second_consumed)
2254        } else {
2255            (second_consumed, first_consumed)
2256        };
2257        let transverse_coedge = CoedgeRecord {
2258            id: take_id(),
2259            edge_id: transverse_id,
2260            forward,
2261            pcurve,
2262        };
2263        splice_end_corner(
2264            result,
2265            end_face,
2266            &located,
2267            transverse_coedge,
2268            x_consumed,
2269            y_consumed,
2270        )?;
2271    }
2272
2273    // Blend face: cr fwd -> TB -> cs rev -> TA rev in (t, z) space.  Both rows
2274    // share one parameterisation, so a collapsed cr reads its sense across the
2275    // strip the cs rail still spans (a stripe with BOTH rails collapsed has no
2276    // area; the orchestrator drops it with the edges that bound it).
2277    let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2278        (Some((_, cr_pcurve)), _) => {
2279            let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2280            (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2281        }
2282        (None, Some(_)) => {
2283            let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2284            (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2285        }
2286        (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2287    };
2288    let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2289    let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2290    let out1 = if first.face.same_sense {
2291        n1
2292    } else {
2293        n1.scale(-1.0)
2294    };
2295    let same_sense = blend_normal.dot(out1) >= 0.0;
2296    let loop_id = take_id();
2297    // Traversal senses of the blend's support coedges.  A fresh support edge
2298    // is parameterized in station order, so the sense is the loop's walking
2299    // direction (`blend_cr_forward` for cr, its opposite for cs); a SEWN
2300    // side's sense comes from the detection (the existing edge's own
2301    // parameter direction relative to that same walk — equal to the dropped
2302    // face's old sense, preserving manifold pairing with the survivor).  The
2303    // pcurves always follow the LOOP's walking direction and are unchanged.
2304    let cr_forward = sew_first
2305        .as_ref()
2306        .map_or(blend_cr_forward, |sew| sew.forward);
2307    let cs_forward = sew_second
2308        .as_ref()
2309        .map_or(!blend_cr_forward, |sew| sew.forward);
2310    // The two end slots of the blend loop.  `natural_forward` is the sense the
2311    // loop walks a transverse edge stored first-rim -> second-rim; a CORNER
2312    // arc was already committed in the walk direction (so the corner patch can
2313    // take it the other way round), so it is always used forward.
2314    // A slot is one coedge for a free end (the transverse curve) or a corner
2315    // end (the section arc), and one or two for a miter end (the seam, then
2316    // the sibling's section when the seam left the sibling first).  A corner
2317    // arc was committed in the walk direction; a miter edge records its own
2318    // sense along the walk.
2319    let mut end_slot = |plan: &EndPlan,
2320                        free_edge_id: Option<u64>,
2321                        natural_forward: bool|
2322     -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2323        match plan {
2324            EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2325                id: take_id(),
2326                edge_id: end.arc_edge_id,
2327                forward: true,
2328                pcurve: end.arc_blend_pcurve.clone(),
2329            }]),
2330            EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2331                id: take_id(),
2332                edge_id: end.arc_edge_id,
2333                forward: true,
2334                pcurve: end.arc_blend_pcurve.clone(),
2335            }]),
2336            EndPlan::Miter(end) => end
2337                .edges
2338                .iter()
2339                .map(|(edge_id, forward, pcurve)| {
2340                    Ok(CoedgeRecord {
2341                        id: take_id(),
2342                        edge_id: *edge_id,
2343                        forward: *forward,
2344                        pcurve: pcurve.clone(),
2345                    })
2346                })
2347                .collect(),
2348            EndPlan::Free(free) => {
2349                let edge_id =
2350                    free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2351                Ok(vec![if natural_forward {
2352                    CoedgeRecord {
2353                        id: take_id(),
2354                        edge_id,
2355                        forward: true,
2356                        pcurve: free.transverse_blend_pcurve.clone(),
2357                    }
2358                } else {
2359                    CoedgeRecord {
2360                        id: take_id(),
2361                        edge_id,
2362                        forward: false,
2363                        pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2364                    }
2365                }])
2366            }
2367        }
2368    };
2369    let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2370    let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2371    let start_slot = match (start_free, &start_split, transverse_a_id) {
2372        (Some(end), Some(commit), Some(id)) => super::split_floor::split_slot(end, end.split.as_ref().expect("planned"), commit, id, !blend_cr_forward, &mut *take_id)?,
2373        _ => start_slot,
2374    };
2375    let finish_slot = match (finish_free, &finish_split, transverse_b_id) {
2376        (Some(end), Some(commit), Some(id)) => super::split_floor::split_slot(end, end.split.as_ref().expect("planned"), commit, id, blend_cr_forward, &mut *take_id)?,
2377        _ => finish_slot,
2378    };
2379    for (end, commit, id, corners, corner, forward) in [
2380        (start_free, &start_split, transverse_a_id, &start_split_corners, old_start, !blend_cr_forward),
2381        (finish_free, &finish_split, transverse_b_id, &finish_split_corners, old_finish, blend_cr_forward),
2382    ] {
2383        if let (Some(end), Some(commit), Some(id), Some(corners)) = (end, commit, id, corners) {
2384            super::split_floor::finish_split_end(result, &mut *take_id, end, end.split.as_ref().expect("planned"), commit, id, corners, corner, forward, name)?;
2385        }
2386    }
2387    // A collapsed rail has no coedge: the end slots on either side of it meet
2388    // at its one rim point.
2389    let coedges = if blend_cr_forward {
2390        let mut coedges = Vec::new();
2391        if let Some(cr_edge_id) = cr_edge_id {
2392            coedges.push(CoedgeRecord {
2393                id: take_id(),
2394                edge_id: cr_edge_id,
2395                forward: cr_forward,
2396                pcurve: crate::sweep_topology::parameter_line(
2397                    cr_domain[0],
2398                    0.0,
2399                    cr_domain[1],
2400                    0.0,
2401                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2402            });
2403        }
2404        coedges.extend(finish_slot);
2405        if let Some(cs_edge_id) = cs_edge_id {
2406            coedges.push(CoedgeRecord {
2407                id: take_id(),
2408                edge_id: cs_edge_id,
2409                forward: cs_forward,
2410                pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2411            });
2412        }
2413        coedges.extend(start_slot);
2414        coedges
2415    } else {
2416        let mut coedges = Vec::new();
2417        if let Some(cr_edge_id) = cr_edge_id {
2418            coedges.push(CoedgeRecord {
2419                id: take_id(),
2420                edge_id: cr_edge_id,
2421                forward: cr_forward,
2422                pcurve: crate::sweep_topology::parameter_line(
2423                    cr_domain[1],
2424                    0.0,
2425                    cr_domain[0],
2426                    0.0,
2427                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2428            });
2429        }
2430        coedges.extend(start_slot);
2431        if let Some(cs_edge_id) = cs_edge_id {
2432            coedges.push(CoedgeRecord {
2433                id: take_id(),
2434                edge_id: cs_edge_id,
2435                forward: cs_forward,
2436                pcurve: crate::sweep_topology::parameter_line(
2437                    cs_domain[0],
2438                    1.0,
2439                    cs_domain[1],
2440                    1.0,
2441                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2442            });
2443        }
2444        coedges.extend(finish_slot);
2445        coedges
2446    };
2447    let blend_face_id = take_id();
2448    let mut blend_face = FaceRecord {
2449        id: blend_face_id,
2450        surface: rows.surface,
2451        same_sense,
2452        loops: vec![LoopRecord {
2453            id: loop_id,
2454            coedges,
2455        }],
2456        name: name.map(|value| value.to_string()),
2457    };
2458    if rows.exact_extrusion {
2459        transpose_face(&mut blend_face)?;
2460    }
2461    let shell_index = result
2462        .shells
2463        .iter()
2464        .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2465        .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2466    // Drop the fully-consumed mate faces (lesson 3): their loops collapsed
2467    // between the sewn edge and the blend; the consumed boundaries and
2468    // lesson-7 leftovers are deleted below, the sewn edge lives on shared by
2469    // the blend face and the surviving neighbour.
2470    for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2471        if sew.is_some() {
2472            for shell in &mut result.shells {
2473                shell.faces.retain(|face| face.id != mate.face.id);
2474            }
2475        }
2476    }
2477    result.shells[shell_index].faces.push(blend_face);
2478
2479    result.edges.retain(|candidate| candidate.id != edge.id);
2480    // Delete the boundary edges a prior fillet's corner had left behind that
2481    // this blend fully consumed.
2482    result
2483        .edges
2484        .retain(|candidate| !consumed_edges.contains(&candidate.id));
2485    // Each collapsed rail's two rims are one vertex.
2486    let mut identified = Vec::new();
2487    for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2488        if collapsed && from != to {
2489            identified.push((from, to));
2490        }
2491    }
2492    // The stopped-end rollover's sides: the rail past the crossing and the
2493    // cap's leg chain move onto the neighbours, and the end footprint is
2494    // re-traced on every face it touches.
2495    let mut cr_edge_id = cr_edge_id;
2496    let mut cs_edge_id = cs_edge_id;
2497    if tail.iter().any(Option::is_some) {
2498        let EndPlan::Cap(cap) = &finish_end else {
2499            return Err(KernelRefusal::unsupported(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the rolled stripe's finish end is not a cap, which is the only end this lane closes on the neighbours"));
2500        };
2501        let mut footprint = StoppedEndFootprint::default();
2502        // The cap plane at the finish vertex: through the sharp vertex, normal
2503        // the edge's tangent there; the fillet lies on the -tangent side.
2504        {
2505            let sharp = result.vertices.iter().find(|vertex| vertex.id == cap.sharp_vertex).map(|vertex| vertex.point)
2506                .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the sharp vertex vanished"))?;
2507            let d = edge.curve.derivatives(edge.t1, 1).or_refuse(KernelStage::Refine, "derivatives")?;
2508            footprint.plane = (sharp, d[1].normalized().or_refuse(KernelStage::Refine, "normalized")?);
2509            footprint.sharp_vertex = cap.sharp_vertex;
2510        }
2511        for (side, splice) in tail.into_iter().enumerate() {
2512            let Some(splice) = splice else { continue };
2513            let (mate, rail_edge_id, rim, sewn) = if side == 0 {
2514                (first, cr_edge_id, w1b, sew_first.is_some())
2515            } else {
2516                (second, cs_edge_id, w2b, sew_second.is_some())
2517            };
2518            let Some(rail_edge_id) = rail_edge_id.filter(|_| !sewn) else {
2519                return Err(KernelRefusal::unsupported(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: a rolled side's rail collapsed or sewed onto an existing edge, which this lane does not split"));
2520            };
2521            let part_on_mate = roll_side_footprint(result, take_id, mate, &splice, rail_edge_id, rim, cap.sharp_vertex, &mut footprint)?;
2522            if side == 0 { cr_edge_id = Some(part_on_mate); } else { cs_edge_id = Some(part_on_mate); }
2523        }
2524        retrace_stopped_end_footprint(result, take_id, footprint)?;
2525    }
2526    Ok(SewnStripe {
2527        cr_edge_id,
2528        cs_edge_id,
2529        blend_face_id,
2530        consumed: [sew_first.is_some(), sew_second.is_some()],
2531        snap,
2532        identified,
2533    })
2534}
2535
2536/// A stopped-end rollover's footprint on the solid, gathered side by side
2537/// and re-traced once ([`retrace_stopped_end_footprint`]).
2538#[derive(Default)]
2539struct StoppedEndFootprint {
2540    /// The new boundary of the footprint, per face: (face, edge, from, to,
2541    /// pcurve from -> to on that face).
2542    pieces: Vec<(u64, u64, u64, u64, NurbsCurve)>,
2543    /// The faces whose loops change: the mates and every face a piece lies on.
2544    faces: Vec<u64>,
2545    /// The cap plane: (sharp point, unit edge tangent at the vertex). The
2546    /// fillet lies on the side opposite the tangent.
2547    plane: (Vec3, Vec3),
2548    sharp_vertex: u64,
2549}
2550
2551/// One rolled side, after the pristine sewing: the rail is split at the
2552/// crossing vertex (on the crossed tangent edge) and the tangent edge there;
2553/// the pristine splice's rail far part and first leg piece leave the mate's
2554/// loop (they belong to the neighbours), and the footprint's new boundary on
2555/// the neighbours — the far part with its pcurve on the neighbour by
2556/// projection, and the cap's leg chain — is recorded for the re-trace.
2557/// Returns the rail part left on the mate.
2558#[allow(clippy::too_many_arguments)]
2559fn roll_side_footprint(
2560    result: &mut BrepSolid,
2561    take_id: &mut dyn FnMut() -> u64,
2562    mate: &BlendMate,
2563    splice: &crate::blend::rollover::TailSplice<'_>,
2564    rail_edge_id: u64,
2565    rim_vertex: u64,
2566    sharp_vertex: u64,
2567    footprint: &mut StoppedEndFootprint,
2568) -> Result<u64, KernelRefusal> {
2569    let what = "stopped_end_rollover";
2570    let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2571    let _ = sharp_vertex;
2572    let rolled = splice.rolled;
2573    let crossing_vertex = take_id();
2574    result.vertices.push(VertexRecord { id: crossing_vertex, point: rolled.point });
2575    let rail_parts = crate::blend::rollover_surgery::split_edge_at_vertices(result, rail_edge_id, &[(crossing_vertex, rolled.point)], take_id)?;
2576    let [rail_near, rail_far] = [rail_parts[0], rail_parts[1]];
2577    let rail_far_edge = result.edges.iter().find(|edge| edge.id == rail_far).cloned()
2578        .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the rail's far part vanished"))?;
2579    if rail_far_edge.end_vertex_id != rim_vertex {
2580        return Err(not_built(format!("the rail's far part ends at vertex {} rather than the finish rim {rim_vertex}", rail_far_edge.end_vertex_id)));
2581    }
2582    // The crossed tangent edge, or the part of it the cap's leg chain left
2583    // holding the crossing (the chain may have split it already).
2584    let holds_crossing = |result: &BrepSolid, id: u64| -> Result<bool, KernelRefusal> {
2585        let Some(edge) = result.edges.iter().find(|edge| edge.id == id) else { return Ok(false) };
2586        let foot = crate::project_point_to_curve(&edge.curve, rolled.point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2587        Ok(foot.distance <= 1e-6 && foot.u > edge.t0.min(edge.t1) && foot.u < edge.t0.max(edge.t1))
2588    };
2589    let crossed = if result.edges.iter().any(|edge| edge.id == rolled.edge_id) {
2590        rolled.edge_id
2591    } else {
2592        let candidates: Vec<u64> = result.edges.iter().map(|edge| edge.id).collect();
2593        let mut found = None;
2594        for id in candidates {
2595            if holds_crossing(result, id)? {
2596                found = Some(id);
2597                break;
2598            }
2599        }
2600        found.ok_or_else(|| not_built(format!("no part of tangent edge {} holds the crossing any more", rolled.edge_id)))?
2601    };
2602    crate::blend::rollover_surgery::split_edge_at_vertices(result, crossed, &[(crossing_vertex, rolled.point)], take_id)?;
2603    // The neighbour's pcurve of the rail's far part, by projection.
2604    let neighbour_surface = result
2605        .shells
2606        .iter()
2607        .flat_map(|shell| &shell.faces)
2608        .find(|face| face.id == rolled.neighbour_face_id)
2609        .map(|face| face.surface.clone())
2610        .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the neighbour face vanished"))?;
2611    let far_pcurve = {
2612        const SAMPLES: usize = 24;
2613        let [t0, t1] = [rail_far_edge.t0, rail_far_edge.t1];
2614        let mut uvs = Vec::with_capacity(SAMPLES + 1);
2615        let mut ts = Vec::with_capacity(SAMPLES + 1);
2616        for k in 0..=SAMPLES {
2617            let t = t0 + (t1 - t0) * k as f64 / SAMPLES as f64;
2618            let point = rail_far_edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
2619            let projection = crate::project_point_to_surface(&neighbour_surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
2620            uvs.push(Vec3::new(projection.u, projection.v, 0.0));
2621            ts.push(t);
2622        }
2623        // Clamped over the rail part's own parameter interval (the edge's).
2624        crate::fit::interpolate_curve(&uvs, 3, &ts).or_refuse(KernelStage::Refine, "interpolate_curve")?
2625    };
2626    // The pristine splice's artefacts leave the mate's loop.
2627    let leg_edge_id = splice.legs.first().map(|piece| piece.edge_id)
2628        .ok_or_else(|| not_built("a rolled side has no cap leg".to_string()))?;
2629    let mate_face = result
2630        .shells
2631        .iter_mut()
2632        .flat_map(|shell| shell.faces.iter_mut())
2633        .find(|face| face.id == mate.face.id)
2634        .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the mate face vanished"))?;
2635    mate_face.loops[mate.loop_index].coedges.retain(|coedge| coedge.edge_id != rail_far && coedge.edge_id != leg_edge_id);
2636    footprint.pieces.push((rolled.neighbour_face_id, rail_far, crossing_vertex, rim_vertex, far_pcurve));
2637    for piece in &splice.legs {
2638        footprint.pieces.push((piece.face_id, piece.edge_id, piece.from, piece.to, piece.pcurve.clone()));
2639    }
2640    for face_id in [mate.face.id, rolled.neighbour_face_id].into_iter().chain(splice.legs.iter().map(|piece| piece.face_id)) {
2641        if !footprint.faces.contains(&face_id) {
2642            footprint.faces.push(face_id);
2643        }
2644    }
2645    Ok(rail_near)
2646}
2647
2648/// Re-trace the footprint: an existing edge of an affected face whose both
2649/// ends lie on the footprint's new boundary and whose midpoint lies on the
2650/// fillet's side of the cap plane is INTERIOR to the footprint and deleted
2651/// (F10: edge 46 between the crossing and the cap leg's exit, edge 68
2652/// between its crossing and vertex 34); then every affected loop is traced
2653/// afresh through its remaining coedges and the new pieces — each vertex
2654/// of the new boundary has exactly one way on — the first closed trace
2655/// keeping the loop, every further one its own face on the same carrier
2656/// (F10: face 13's triangle beyond the cap plane).
2657fn retrace_stopped_end_footprint(
2658    result: &mut BrepSolid,
2659    take_id: &mut dyn FnMut() -> u64,
2660    footprint: StoppedEndFootprint,
2661) -> Result<(), KernelRefusal> {
2662    let what = "stopped_end_rollover";
2663    let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2664    let (sharp_point, tangent) = footprint.plane;
2665    let mut boundary_vertices: Vec<u64> = vec![footprint.sharp_vertex];
2666    for (_, _, from, to, _) in &footprint.pieces {
2667        for vertex in [*from, *to] {
2668            if !boundary_vertices.contains(&vertex) {
2669                boundary_vertices.push(vertex);
2670            }
2671        }
2672    }
2673    let piece_edges: Vec<u64> = footprint.pieces.iter().map(|piece| piece.1).collect();
2674    // Interior edges, among those on the affected faces.
2675    let mut interior: Vec<u64> = Vec::new();
2676    for face in result.shells.iter().flat_map(|shell| &shell.faces).filter(|face| footprint.faces.contains(&face.id)) {
2677        for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
2678            let Some(edge) = result.edges.iter().find(|edge| edge.id == coedge.edge_id) else { continue };
2679            if piece_edges.contains(&edge.id) || interior.contains(&edge.id) {
2680                continue;
2681            }
2682            if !(boundary_vertices.contains(&edge.start_vertex_id) && boundary_vertices.contains(&edge.end_vertex_id)) {
2683                continue;
2684            }
2685            let mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).or_refuse(KernelStage::Refine, "evaluate")?;
2686            if mid.sub(sharp_point).dot(tangent) < 0.0 {
2687                interior.push(edge.id);
2688            }
2689        }
2690    }
2691    if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
2692        eprintln!("stopped-end rollover: footprint faces {:?}, interior edges {:?}, boundary vertices {:?}", footprint.faces, interior, boundary_vertices);
2693    }
2694    let edge_ends: std::collections::HashMap<u64, (u64, u64)> = result.edges.iter().map(|edge| (edge.id, (edge.start_vertex_id, edge.end_vertex_id))).collect();
2695    let mut new_faces: Vec<FaceRecord> = Vec::new();
2696    // Face names the result uses (the app keys faces by name; this model
2697    // already carries `_1` splits of its own): a remnant takes a free suffix.
2698    let mut taken: Vec<String> = result.shells.iter().flat_map(|shell| &shell.faces).filter_map(|face| face.name.clone()).collect();
2699    for face_id in footprint.faces.clone() {
2700        let face = result
2701            .shells
2702            .iter_mut()
2703            .flat_map(|shell| shell.faces.iter_mut())
2704            .find(|face| face.id == face_id)
2705            .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: face {face_id} vanished")))?;
2706        let pieces: Vec<&(u64, u64, u64, u64, NurbsCurve)> = footprint.pieces.iter().filter(|piece| piece.0 == face_id).collect();
2707        let mut rebuilt_loops: Vec<LoopRecord> = Vec::new();
2708        for loop_record in &face.loops {
2709            let touched = loop_record.coedges.iter().any(|coedge| interior.contains(&coedge.edge_id))
2710                || loop_record.coedges.iter().any(|coedge| {
2711                    edge_ends.get(&coedge.edge_id).is_some_and(|(a, b)| boundary_vertices.contains(a) || boundary_vertices.contains(b))
2712                });
2713            if !touched {
2714                rebuilt_loops.push(loop_record.clone());
2715                continue;
2716            }
2717            // Directed remaining coedges (direction fixed, with their place in
2718            // the old loop) and undirected pieces.
2719            let n = loop_record.coedges.len();
2720            let mut remaining: Vec<(CoedgeRecord, u64, u64, usize)> = Vec::new();
2721            for (index, coedge) in loop_record.coedges.iter().enumerate() {
2722                if interior.contains(&coedge.edge_id) {
2723                    continue;
2724                }
2725                let (start, end) = edge_ends.get(&coedge.edge_id).copied()
2726                    .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: coedge {} names a missing edge {}", coedge.id, coedge.edge_id)))?;
2727                let (from, to) = if coedge.forward { (start, end) } else { (end, start) };
2728                remaining.push((coedge.clone(), from, to, index));
2729            }
2730            let mut loose: Vec<&(u64, u64, u64, u64, NurbsCurve)> = pieces.clone();
2731            // Trace closed walks: start at a remaining coedge, follow the
2732            // continuation at each vertex — the old loop's own successor when
2733            // it is there (a periodic face's loop passes its seam vertex
2734            // twice), else the unique way on.
2735            while !remaining.is_empty() {
2736                // Start on a coedge that is not a degenerate self-loop (a
2737                // periodic face's pole edge runs from a vertex to itself).
2738                let start_at = remaining.iter().position(|(_, from, to, _)| from != to).unwrap_or(0);
2739                let (first, from0, to0, index0) = remaining.remove(start_at);
2740                let mut walk: Vec<CoedgeRecord> = vec![first];
2741                let mut at = to0;
2742                let mut previous = index0;
2743                let mut guard = 0usize;
2744                loop {
2745                    guard += 1;
2746                    if guard > 4096 {
2747                        return Err(not_built(format!("face {face_id}: the re-trace does not close")));
2748                    }
2749                    let mut next_remaining: Vec<usize> = remaining.iter().enumerate().filter(|(_, (_, from, _, _))| *from == at).map(|(k, _)| k).collect();
2750                    let successor_is_start = (previous + 1) % n == index0;
2751                    if at == from0 && (successor_is_start || next_remaining.is_empty()) {
2752                        // Back at the start along the old loop's own order (a
2753                        // seam vertex passed twice does not close it early).
2754                        if at == from0 && successor_is_start {
2755                            break;
2756                        }
2757                        if loose.iter().all(|piece| piece.2 != at && piece.3 != at) {
2758                            break;
2759                        }
2760                    }
2761                    if next_remaining.len() > 1 {
2762                        if let Some(k) = next_remaining.iter().copied().find(|k| remaining[*k].3 == (previous + 1) % n) {
2763                            next_remaining = vec![k];
2764                        }
2765                    }
2766                    let next_pieces: Vec<usize> = loose.iter().enumerate().filter(|(_, piece)| piece.2 == at || piece.3 == at).map(|(k, _)| k).collect();
2767                    match (next_remaining.len(), next_pieces.len()) {
2768                        (1, 0) => {
2769                            let (coedge, _, to, index) = remaining.remove(next_remaining[0]);
2770                            walk.push(coedge);
2771                            at = to;
2772                            previous = index;
2773                        }
2774                        (0, 1) => {
2775                            let piece = loose.remove(next_pieces[0]);
2776                            let (edge_id, from, to, pcurve) = (piece.1, piece.2, piece.3, &piece.4);
2777                            let (start, _) = edge_ends.get(&edge_id).copied()
2778                                .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: footprint edge {edge_id} missing")))?;
2779                            let along = from == at;
2780                            let coedge_forward = if along { start == from } else { start == to };
2781                            walk.push(CoedgeRecord {
2782                                id: take_id(),
2783                                edge_id,
2784                                forward: coedge_forward,
2785                                pcurve: if along { pcurve.clone() } else { pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2786                            });
2787                            at = if along { to } else { from };
2788                        }
2789                        (r, p) => {
2790                            return Err(not_built(format!("face {face_id}: vertex {at} has {} ways on ({r} boundary, {p} new), not one", r + p)));
2791                        }
2792                    }
2793                }
2794                rebuilt_loops.push(LoopRecord { id: if rebuilt_loops.is_empty() { loop_record.id } else { take_id() }, coedges: walk });
2795            }
2796            if !loose.is_empty() {
2797                return Err(not_built(format!("face {face_id}: {} new boundary piece(s) joined no loop", loose.len())));
2798            }
2799        }
2800        // The first loop keeps the face; every further closed trace of a
2801        // touched loop is a face of its own on the same carrier.
2802        let kept = rebuilt_loops.remove(0);
2803        face.loops = vec![kept];
2804        let surface = face.surface.clone();
2805        let same_sense = face.same_sense;
2806        let base_name = face.name.clone();
2807        for extra in rebuilt_loops {
2808            let name = base_name.as_ref().map(|base| {
2809                (1..).map(|k| format!("{base}_{k}")).find(|candidate| !taken.contains(candidate)).expect("a free name")
2810            });
2811            if let Some(name) = &name {
2812                taken.push(name.clone());
2813            }
2814            new_faces.push(FaceRecord { id: take_id(), surface: surface.clone(), same_sense, loops: vec![extra], name });
2815        }
2816    }
2817    if !new_faces.is_empty() {
2818        let shell_index = result.shells.iter().position(|shell| !shell.faces.is_empty())
2819            .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: no shell"))?;
2820        result.shells[shell_index].faces.extend(new_faces);
2821    }
2822    result.edges.retain(|edge| !interior.contains(&edge.id));
2823    Ok(())
2824}
2825
2826/// Swap the two parameters of a face: the surface's control net, every
2827/// coedge pcurve, and the sense (S_u × S_v changes sign).  The face's
2828/// geometry is untouched; only its chart is.  Used to hand an exact
2829/// cylinder patch built with its section along v to the analytic recogniser,
2830/// which wants the circle along u.
2831pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2832    let surface = &face.surface;
2833    let rows_u = surface.control_points.len();
2834    let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2835    let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2836    for row in &surface.control_points {
2837        for (j, point) in row.iter().enumerate() {
2838            transposed[j].push(*point);
2839        }
2840    }
2841    face.surface = NurbsSurface::new(
2842        surface.degree_v,
2843        surface.degree_u,
2844        surface.knots_v.clone(),
2845        surface.knots_u.clone(),
2846        transposed,
2847    ).or_refuse(KernelStage::Refine, "new")?;
2848    for loop_record in &mut face.loops {
2849        for coedge in &mut loop_record.coedges {
2850            for control in &mut coedge.pcurve.control_points {
2851                std::mem::swap(&mut control.x, &mut control.y);
2852            }
2853        }
2854    }
2855    face.same_sense = !face.same_sense;
2856    Ok(())
2857}
2858
2859/// Drop every vertex no longer referenced by an edge — the sharp corners the
2860/// blends replaced, and anything freed by a consumed boundary.  Run ONCE after
2861/// a whole group of stripes is in: a corner vertex is still referenced by the
2862/// selected edges that have not been sewn yet.
2863pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2864    let used: rustc_hash::FxHashSet<u64> = result
2865        .edges
2866        .iter()
2867        .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2868        .collect();
2869    result
2870        .vertices
2871        .retain(|candidate| used.contains(&candidate.id));
2872}
2873
2874fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2875    crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2876}
2877
2878
2879