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 (stations, vertex_indices, refinement, dense) = march_open_stations_core(
105            edge,
106            &first_mate,
107            &second_mate,
108            radius_at,
109            overshoot_fraction,
110            [edge.t0, edge.t1],
111            station_count,
112            [false, false],
113            [false, false],
114            crate::blend::station_refinement_on().then_some(rail_bar),
115            if wall_check { OPEN_WALL_CENTRES } else { 0 },
116            true,
117        )?;
118        let parameters = station_parameters(&stations);
119        let rows = fit_open_rows(
120            &stations,
121            &parameters,
122            chamfer,
123            Some(vertex_indices),
124            extrusion_direction(edge, &first_mate, &second_mate),
125        )?;
126        // The fitted wall itself must not double back along a row
127        // (`fold::check_rows_loop`); terminal, as the march's fold verdicts.
128        crate::blend::fold::check_rows_loop(
129            &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),
130        )?;
131        let mut ends = Vec::with_capacity(2);
132        let mut in_range = true;
133        for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
134            let (end, side_in_range) = resolve_free_end(
135                solid,
136                edge_id,
137                &first_mate,
138                &second_mate,
139                &rows,
140                vertex,
141                at_start,
142            )?;
143            if debug {
144                eprintln!(
145                    "  [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}",
146                    end.end_face_id,
147                    end.first_edge_id,
148                    end.first_edge_parameter,
149                    end.second_edge_id,
150                    end.second_edge_parameter,
151                    end.cr_parameter,
152                    end.cs_parameter,
153                );
154            }
155            in_range &= side_in_range;
156            ends.push(end);
157        }
158        // The measurement covers what SHIPS: each rail over its own crossing
159        // window (the `cr`/`cs` parameters `build_open_surgery` trims it to,
160        // any retained overshoot included), and the wall over the envelope of
161        // both windows -- not merely between the vertex stations.
162        let windows = [
163            shipped_window(&parameters, ends[0].cr_parameter, ends[1].cr_parameter),
164            shipped_window(&parameters, ends[0].cs_parameter, ends[1].cs_parameter),
165        ];
166        let (rails, rail_samples) =
167            rails_on_shipped_windows(&rows, &parameters, &stations, [&first_mate.face.surface, &second_mate.face.surface], windows);
168        let walls = match (wall_check, windows) {
169            (false, _) => Vec::new(),
170            (true, [Some(first), Some(second)]) => open_wall_readings_why(
171                &rows,
172                &parameters,
173                &stations,
174                [first[0].min(second[0]), first[1].max(second[1])],
175                &dense,
176                wall_radius.unwrap_or(f64::NAN),
177            ),
178            // No shipped window to read: no wall coverage, never acceptance.
179            (true, _) => vec![Err("no shipped window to read".to_string())],
180        };
181        let first_unread = walls.iter().find_map(|wall| wall.as_ref().err().cloned());
182        let walls: Vec<Option<f64>> = if wall_check && windows.iter().any(|window| window.is_none()) {
183            Vec::new()
184        } else {
185            walls.into_iter().map(|wall| wall.ok()).collect()
186        };
187        let reading = OpenRungReading {
188            station_count,
189            stations: stations.len(),
190            rails,
191            rail_samples,
192            walls,
193            first_unread,
194            refine_exit: refinement.exit,
195            inserted: refinement.inserted,
196        };
197        Ok((rows, ends, in_range, reading))
198    };
199
200    // Radius-aware seeded first attempt (occt-filleting-system-study §6(b)
201    // lesson 7): OCCT floors corner extensions at 1.5·max_radius
202    // (`ExtentTwoCorner`) instead of probing blindly.  Convert that floor to
203    // an overshoot fraction of THIS edge: the contact rails sit r·tan(α/2)
204    // from the edge (α = sign-adjusted angle between the mates' raw normals
205    // at mid-edge — the march's own `cos_alpha`), so seed with 1.5× the
206    // widest end radius's rail offset over the edge arc length, clamped to
207    // the ladder's proven [0.08, 0.45] envelope.  Deterministic; `None` when
208    // it degenerates or merely reproduces the ladder's first rung.
209    let seeded_fraction: Option<f64> = (|| {
210        let span = edge.t1 - edge.t0;
211        let mut length = 0.0f64;
212        let mut previous = edge.curve.evaluate(edge.t0).ok()?;
213        for index in 1..=16 {
214            let t = edge.t0 + span * index as f64 / 16.0;
215            let point = edge.curve.evaluate(t).ok()?;
216            length += point.sub(previous).length();
217            previous = point;
218        }
219        if !(length > 0.0) || !length.is_finite() {
220            return None;
221        }
222        let mid_t = edge.t0 + span * 0.5;
223        let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
224        let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
225        let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
226        let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
227        let cos_alpha =
228            (rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
229        // tan(α/2) = √((1−cosα)/(1+cosα)); the tangent-offset factor from
230        // the edge to each rail (box: α = π/2 → offset = r).
231        let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
232        let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
233        let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
234        if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
235            return None;
236        }
237        Some(fraction)
238    })();
239
240    // Retry with a growing overshoot; keep the first attempt whose crossings
241    // are all in range, else fall back to the widest march tried.  The seeded
242    // attempt is accepted ONLY when its crossings land in range — otherwise
243    // the blind ladder below runs exactly as before (including its
244    // fallback-to-first-Ok semantics), so the seed can improve the first
245    // landing but never change the fallback behaviour.
246    let choose = |station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, OpenRungReading), KernelRefusal> {
247    let mut chosen: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
248    let mut last_error: Option<KernelRefusal> = None;
249    // A wall that FOLDS is the shape's answer, not this rung's: every wider
250    // overshoot marches the same centre curve through the same bend, so the
251    // ladder is left at the first fold rather than re-proving it four times.
252    if let Some(fraction) = seeded_fraction {
253        match compute(fraction, station_count) {
254            Ok((rows, ends, true, reading)) => chosen = Some((rows, ends, reading)),
255            Ok(_) => {}
256            Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
257            Err(error) => last_error = Some(error),
258        }
259    }
260    if chosen.is_none() {
261        for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
262            match compute(overshoot_fraction, station_count) {
263                Ok((rows, ends, in_range, reading)) => {
264                    let fallback = chosen.is_none();
265                    if in_range {
266                        chosen = Some((rows, ends, reading));
267                        break;
268                    } else if fallback {
269                        chosen = Some((rows, ends, reading));
270                    }
271                }
272                Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
273                Err(error) => last_error = Some(error),
274            }
275        }
276    }
277    chosen.ok_or_else(|| {
278        last_error.unwrap_or_else(|| {
279            KernelRefusal::internal(
280                KernelStage::Refine,
281                "open_march",
282                "blend: open march failed at every overshoot",
283            )
284        })
285    })
286    };
287    // The LADDER: the open march's existing budget — STATIONS doubling to
288    // MAX_OPEN_STATIONS, each rung with its local refinement (REFINE_ROUNDS,
289    // REFINE_STATION_FACTOR) against the rail bar — climbed until a rung is
290    // ACCEPTED (rails and wall inside their bars), then, for a plain fillet,
291    // further toward `model` on the wall. A rung the request climbs to that
292    // fails acceptance is not taken: the accepted rung ships and says so,
293    // typed. A ladder that never accepts ships its top rung with a named note,
294    // as the network lane's open march does (a body that built before is not
295    // turned into a refusal here) -- but it is NOT an accepted rung: it ships
296    // with a typed `blend.wall_acceptance` report of its measured deficit.
297    // Acceptance is stated positively: every reading present, finite and
298    // inside its bar (a NaN or an empty coverage never passes).
299    let accepts = |reading: &OpenRungReading| {
300        reading.rails_read()
301            && reading.rails <= rail_bar
302            && (!wall_check || reading.worst_wall().is_some_and(|wall| wall <= wall_bar))
303    };
304    let meets_model = |reading: &OpenRungReading| reading.worst_wall().is_some_and(|wall| wall <= model);
305    let mut station_count = STATIONS;
306    let mut accepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
307    // The top rung of a ladder that NEVER accepted: shipped as built, kept
308    // apart from `accepted` so it is never reported as an accepted parent.
309    let mut unaccepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
310    let mut request_rungs = 0usize;
311    let mut unmet: Option<crate::BudgetReason> = None;
312    loop {
313        // A rung climbed to after acceptance is a request rung ATTEMPTED,
314        // whatever it returns.
315        if accepted.is_some() {
316            request_rungs += 1;
317        }
318        let chosen = choose(station_count);
319        let (rows, ends, reading) = match chosen {
320            Ok(chosen) => chosen,
321            // A request rung that cannot be built: the accepted rung stands.
322            Err(_) if accepted.is_some() => {
323                unmet = Some(crate::BudgetReason::Incoherent);
324                break;
325            }
326            Err(error) => return Err(error),
327        };
328        #[cfg(not(test))]
329        let reject_request = false;
330        crate::blend::carve::carve_trace(format_args!(
331            "blend open edge {}: {} stations a rung ({} in all), rails {:.3e} (bar {rail_bar:.3e}), wall {:.3e} (bar {wall_bar:.3e}){}",
332            edge.id, reading.station_count, reading.stations, reading.rails,
333            reading.worst_wall().unwrap_or(f64::INFINITY),
334            if accepted.is_some() { ", a request rung" } else { "" }
335        ));
336        let accepted_now = accepts(&reading) && !reject_request;
337        if accepted.is_none() {
338            if accepted_now {
339                let met = !wall_check || meets_model(&reading);
340                accepted = Some((rows, ends, reading));
341                if met {
342                    break;
343                }
344            } else if station_count < MAX_OPEN_STATIONS {
345                station_count *= 2;
346                continue;
347            } else {
348                crate::blend::record_blend_note(format!(
349                    "blend: open edge {} at {} stations a rung ({} in all) — the top of the open march's \
350                     ladder — still leaves its rails {:.3e} from their carriers (bar {rail_bar:.3e}) or its \
351                     wall off the rolling ball (bar {wall_bar:.3e}); the wall ships as built, unaccepted",
352                    edge.id, reading.station_count, reading.stations, reading.rails
353                ));
354                unaccepted = Some((rows, ends, reading));
355                break;
356            }
357        } else {
358            if !accepted_now {
359                unmet = Some(crate::BudgetReason::Rejected);
360                break;
361            }
362            let met = meets_model(&reading);
363            accepted = Some((rows, ends, reading));
364            if met {
365                unmet = None;
366                break;
367            }
368        }
369        // Accepted, short of `model`: one more rung if the budget allows.
370        if station_count < MAX_OPEN_STATIONS {
371            station_count *= 2;
372        } else {
373            unmet = Some(crate::BudgetReason::StationCeiling);
374            break;
375        }
376    }
377    let (shipped_accepted, (rows, ends, reading)) = match (accepted, unaccepted) {
378        (Some(rung), _) => (true, rung),
379        (None, Some(rung)) => (false, rung),
380        (None, None) => {
381            return Err(KernelRefusal::internal(KernelStage::Refine, "open_ladder", "blend: the open ladder ended without a rung"));
382        }
383    };
384    // What the shipped wall owes, typed: an UNACCEPTED rung its measured
385    // acceptance deficit (or that a reading could not be taken); an accepted
386    // fillet short of `model` its construction request.
387    let report: Option<(crate::BudgetReason, f64, f64, String)> = if !shipped_accepted {
388        Some(acceptance_deficit(&reading, rail_bar, wall_bar, wall_check, reading.rails_read(), "no rung of the open ladder was accepted"))
389    } else if wall_check && !meets_model(&reading) {
390        let reason = unmet.unwrap_or(crate::BudgetReason::StationCeiling);
391        // An accepted rung's every wall interval was read (acceptance needs it).
392        let declared = reading.worst_wall().unwrap_or(f64::INFINITY);
393        Some((reason, model, declared, String::new()))
394    } else {
395        None
396    };
397
398    let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
399        Ok(pair) => pair,
400        Err(_) => return Err(KernelRefusal::internal(KernelStage::Sew, "open_surgery_ends", "blend: open surgery needs exactly two ends")),
401    };
402    // A crossing that only resolved on a looser rung of the tolerance ladder
403    // builds something master refused outright, so it must earn its answer
404    // (see [`SpokeCrossings`]).
405    let mut crossings = SpokeCrossings::default();
406    crossings.note(start_end.escalated || finish_end.escalated);
407    let mut result = solid.clone();
408    let mut take_id = fresh_id_source(solid);
409    let sewn = build_open_surgery(
410        solid,
411        &mut result,
412        &mut take_id,
413        edge,
414        &first_mate,
415        &second_mate,
416        rows,
417        [EndPlan::Free(start_end), EndPlan::Free(finish_end)],
418        name,
419    )?;
420    prune_orphan_vertices(&mut result);
421    // Extended end boundaries can leave a finite planar chart. Rebuild those
422    // charts before validating the surgery, rather than rejecting a correct
423    // edge because its projected pcurve was clamped to the old rectangle.
424    crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
425    check_snap_closure(solid, &result, sewn.snap)?;
426    let built = crossings.gate(result)?;
427    // The wall AS SEWN: the surgery may transpose the face (an exact
428    // extrusion), and a report is matched to its face by exact surface.
429    let sewn_surface = built
430        .shells
431        .iter()
432        .flat_map(|shell| &shell.faces)
433        .find(|face| face.id == sewn.blend_face_id)
434        .map(|face| face.surface.clone())
435        .ok_or(KernelRefusal::internal(KernelStage::Sew, "open_blend_face", "blend: the sewn open blend face is not in the built body"))?;
436    // A wall that SHIPS unaccepted, or short of its construction request,
437    // says so, typed, on the built body's wall (exact name and sewn surface).
438    if let Some((reason, bar, measured, deficit)) = report {
439        // The budget is the open march's GLOBAL station ladder (STATIONS
440        // doubling to MAX_OPEN_STATIONS), not local rounds: for a request,
441        // the rungs ATTEMPTED past the accepted one (a refused one included);
442        // for an unaccepted rung, the rungs climbed past the first. Each rung
443        // also ran its own local rail refinement, named in the detail.
444        let rungs_used = if shipped_accepted { request_rungs } else { (reading.station_count / STATIONS).trailing_zeros() as usize };
445        let mut detail = format!(
446            "the shipped rung's local rail refinement ended {:?} with {} stations inserted",
447            reading.refine_exit, reading.inserted
448        );
449        if !deficit.is_empty() {
450            detail = format!("{deficit}; {detail}");
451        }
452        crate::blend::record_wall_model_report(crate::blend::WallModelReport {
453            name: name.map(str::to_string),
454            surface: sewn_surface,
455            request: bar,
456            residual: measured,
457            detail: Some(detail),
458            budget: crate::ApproximationBudget {
459                reason,
460                rounds_used: rungs_used,
461                rounds_limit: OPEN_REQUEST_RUNGS,
462                stations: reading.stations,
463                station_limit: REFINE_STATION_FACTOR * (MAX_OPEN_STATIONS + 1),
464                mechanism: crate::BudgetMechanism::StationRungs,
465                measured_component: crate::MeasuredComponent::Wall,
466                unread: 0,
467            },
468        });
469    }
470    Ok(built)
471}
472
473/// An UNACCEPTED shipped rung's deficit, typed: `Unread` when any reading
474/// acceptance needs could not be taken (a non-finite rail, an empty shipped
475/// window, an unread or missing wall interval), else `Unaccepted`; the
476/// measured value and its bar are the readable component worst against its
477/// bar (rails against `rail_bar`, the wall against `wall_bar`; 0 against
478/// `rail_bar` when nothing could be read), always finite. The text names both
479/// readings against their bars, after `what` (what failed acceptance: the
480/// open ladder's every rung, or a network stripe's shipped windows).
481/// `rails_read` is the caller's verdict on whether the rails were READ (the
482/// open lane: [`OpenRungReading::rails_read`]; a network stripe also counts a
483/// validated collapsed rail as read with no samples).
484pub(in crate::blend) fn acceptance_deficit(
485    reading: &OpenRungReading,
486    rail_bar: f64,
487    wall_bar: f64,
488    wall_check: bool,
489    rails_read: bool,
490    what: &str,
491) -> (crate::BudgetReason, f64, f64, String) {
492    let rails = (rails_read && reading.rails.is_finite() && reading.rails >= 0.0).then_some(reading.rails);
493    let wall = if wall_check { reading.worst_wall() } else { None };
494    let unread = rails.is_none() || (wall_check && wall.is_none());
495    let mut worst = (0.0_f64, rail_bar);
496    for (value, bar) in [(rails, rail_bar), (wall, wall_bar)] {
497        if let Some(value) = value {
498            if value / bar > worst.0 / worst.1 {
499                worst = (value, bar);
500            }
501        }
502    }
503    let rail_text = match rails {
504        Some(value) => format!("rails {value:.3e} from their carriers (bar {rail_bar:.3e})"),
505        None => format!(
506            "rails UNREAD ({:?} over {} + {} samples) against bar {rail_bar:.3e}",
507            reading.rails, reading.rail_samples[0], reading.rail_samples[1]
508        ),
509    };
510    let wall_text = match (wall_check, wall) {
511        (false, _) => "the wall not judged (no constant-radius fillet provenance)".to_string(),
512        (true, Some(value)) => format!("wall {value:.3e} off the rolling ball (bar {wall_bar:.3e})"),
513        (true, None) => format!(
514            "wall UNREAD on {} of {} intervals against bar {wall_bar:.3e}{}",
515            reading.walls.iter().filter(|wall| !matches!(wall, Some(value) if value.is_finite())).count(),
516            reading.walls.len(),
517            reading.first_unread.as_deref().map(|why| format!(" (first: {why})")).unwrap_or_default()
518        ),
519    };
520    let reason = if unread { crate::BudgetReason::Unread } else { crate::BudgetReason::Unaccepted };
521    (reason, worst.1, worst.0, format!("{what}: {rail_text}, {wall_text}"))
522}
523
524/// Section probes per open wall interval (k/8 of the section), and centres
525/// per interval (probes at the even ones), as the chains read their walls.
526const OPEN_WALL_SECTIONS: usize = 8;
527pub(in crate::blend) const OPEN_WALL_CENTRES: usize = 32;
528/// The rungs the construction request may climb past the first accepted one:
529/// the open march's own ladder, STATIONS doubling to MAX_OPEN_STATIONS.
530pub(in crate::blend) const OPEN_REQUEST_RUNGS: usize = (MAX_OPEN_STATIONS / STATIONS).trailing_zeros() as usize;
531
532/// One open rung's measurement over what it SHIPS: its rung, its station
533/// count (refinement included), the rails' worst distance from their
534/// carriers over each rail's own crossing window and the samples read on
535/// each (0: no window), each wall interval's declared reading (`None`:
536/// unreadable) over the windows' envelope, and how the rung's own LOCAL rail
537/// refinement ended (its exit and the stations it inserted).
538#[derive(Clone, Debug)]
539pub(in crate::blend) struct OpenRungReading {
540    pub(in crate::blend) station_count: usize,
541    pub(in crate::blend) stations: usize,
542    pub(in crate::blend) rails: f64,
543    pub(in crate::blend) rail_samples: [usize; 2],
544    pub(in crate::blend) walls: Vec<Option<f64>>,
545    /// The first unread wall interval and why (diagnostic; `None` when every
546    /// interval read or the wall was not judged).
547    pub(in crate::blend) first_unread: Option<String>,
548    pub(in crate::blend) refine_exit: crate::blend::stations::StationRefineExit,
549    pub(in crate::blend) inserted: usize,
550}
551
552impl OpenRungReading {
553    /// Every rail READ: a finite, nonnegative worst distance from samples on
554    /// both rails' shipped windows. A NaN, an infinity (an unreadable foot)
555    /// or an empty window is never a reading.
556    pub(in crate::blend) fn rails_read(&self) -> bool {
557        self.rails.is_finite() && self.rails >= 0.0 && self.rail_samples.iter().all(|&samples| samples > 0)
558    }
559
560    /// Every wall interval READ (and at least one): `None` when any is not.
561    pub(in crate::blend) fn worst_wall(&self) -> Option<f64> {
562        if self.walls.is_empty() {
563            return None;
564        }
565        self.walls.iter().try_fold(0.0_f64, |worst, wall| match wall {
566            Some(declared) if declared.is_finite() && *declared >= 0.0 => Some(worst.max(*declared)),
567            _ => None,
568        })
569    }
570}
571
572/// The shipped window `[low, high]` of one rail between its two crossing
573/// parameters, within the rows' parameters (the surgery refuses a crossing
574/// outside them; the reading only never indexes past them). `None` when the
575/// window is empty or not finite: nothing ships there that could be read.
576pub(in crate::blend) fn shipped_window(parameters: &[f64], a: f64, b: f64) -> Option<[f64; 2]> {
577    let (&first, &last) = (parameters.first()?, parameters.last()?);
578    let (low, high) = (a.min(b).max(first), a.max(b).min(last));
579    (low.is_finite() && high.is_finite() && low < high).then_some([low, high])
580}
581
582/// The station interval holding `u` and `u`'s fraction of it.
583fn interval_at(parameters: &[f64], u: f64) -> Option<(usize, f64)> {
584    let last = parameters.len().checked_sub(2)?;
585    let interval = parameters.partition_point(|&parameter| parameter <= u).saturating_sub(1).min(last);
586    let (from, to) = (parameters[interval], parameters[interval + 1]);
587    (to > from).then(|| (interval, ((u - from) / (to - from)).clamp(0.0, 1.0)))
588}
589
590/// Lane R2 row 2a (2026-10-18): `BREP_BLEND_AFFINE_READS=1`/`on` reads the
591/// rails of a straight stripe on a PLANE at their window ends
592/// ([`affine_rail_standoff`], [`exact_planar_trim`]'s read; the default since
593/// its census: 98 + 144 cases identical in every field), `=0`/`off` samples
594/// them as before; `set_affine_reads_for_test` forces it on a test's own
595/// thread.
596const AFFINE_READS_DEFAULT: bool = true;
597
598thread_local! {
599    static AFFINE_READS_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
600}
601
602pub(in crate::blend) fn affine_reads_on() -> bool {
603    if let Some(forced) = AFFINE_READS_FOR_TEST.with(|forced| forced.get()) {
604        return forced;
605    }
606    match std::env::var("BREP_BLEND_AFFINE_READS").as_deref() {
607        Ok("0") | Ok("off") => false,
608        Ok("1") | Ok("on") => true,
609        _ => AFFINE_READS_DEFAULT,
610    }
611}
612
613
614/// A point of `surface` and its unit normal when the surface is AFFINE in
615/// (u, v), its extension included: a plane (`analytic`) of degree 1 × 1 whose
616/// 2 × 2 net is a parallelogram with equal weights, to `1e-12·(1 + scale)`.
617/// Every point of such a surface, and of its linear extension, lies on the one
618/// plane through the corner with that normal.
619fn affine_plane(surface: &NurbsSurface) -> Option<(Vec3, Vec3)> {
620    let (origin, _, _) = affine_plane_frame(surface)?;
621    // The normal off the net's own corner differences.
622    let net = &surface.control_points;
623    let [p00, p01, p10] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?];
624    Some((origin, p10.sub(p00).cross(p01.sub(p00)).normalized().ok()?))
625}
626
627/// [`affine_plane`]'s surface as `S(u, v) = origin + (u − u₀)·s_u + (v − v₀)·s_v`:
628/// the net's corner and the two constant partial derivatives.
629pub(in crate::blend) fn affine_plane_frame(surface: &NurbsSurface) -> Option<(Vec3, Vec3, Vec3)> {
630    if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || surface.degree_u != 1 || surface.degree_v != 1 {
631        return None;
632    }
633    let net = &surface.control_points;
634    if net.len() != 2 || net.iter().any(|row| row.len() != 2) {
635        return None;
636    }
637    let weight = net[0][0].w;
638    if !(weight > 0.0) || net.iter().flatten().any(|point| point.w != weight) {
639        return None;
640    }
641    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()?];
642    let scale = [p00, p01, p10, p11].iter().fold(0.0f64, |worst, point| worst.max(point.length()));
643    if p11.sub(p10).sub(p01.sub(p00)).length() > 1e-12 * (1.0 + scale) {
644        return None;
645    }
646    let [u0, u1] = surface.domain_u().ok()?;
647    let [v0, v1] = surface.domain_v().ok()?;
648    if !(u1 > u0 && v1 > v0) {
649        return None;
650    }
651    Some((p00, p10.sub(p00).scale(1.0 / (u1 - u0)), p01.sub(p00).scale(1.0 / (v1 - v0))))
652}
653
654/// Whether `curve` is AFFINE in its parameter: degree 1, two control points of
655/// equal weight (a `make_line`).
656fn affine_curve(curve: &NurbsCurve) -> bool {
657    curve.degree == 1
658        && curve.control_points.len() == 2
659        && curve.control_points[0].w > 0.0
660        && curve.control_points[0].w == curve.control_points[1].w
661}
662
663/// The worst distance of an affine rail from an affine carrier over `window`,
664/// read at the window's two ends: the signed distance is affine in the rail
665/// parameter, so its magnitude is convex and its maximum is at an end. `None`
666/// when either is not affine (the sampled reader's), or an end will not
667/// evaluate.
668pub(in crate::blend) fn affine_rail_standoff(rail: &NurbsCurve, carrier: &NurbsSurface, window: [f64; 2]) -> Option<f64> {
669    if !affine_curve(rail) {
670        return None;
671    }
672    let (origin, normal) = affine_plane(carrier)?;
673    let mut worst = 0.0f64;
674    for u in window {
675        let point = rail.evaluate(u).ok()?;
676        let off = point.sub(origin).dot(normal).abs();
677        if !off.is_finite() {
678            return None;
679        }
680        worst = worst.max(off);
681    }
682    Some(worst)
683}
684
685/// Each shipped rail's worst distance from its carrier over ITS OWN window
686/// ([`shipped_window`]): both window ends exactly and the quarter points of
687/// every station span inside it, each against the carrier's EXTENSION
688/// (`extended_foot_distance`, seeded by the stations' own contact uv) --
689/// a finite planar chart clamps its projection, so retained overshoot past a
690/// vertex would read a miss that is not there. `(worst, samples per rail)`;
691/// any unreadable sample (a failed evaluation, an infinite foot) makes the
692/// worst NaN, which [`OpenRungReading::rails_read`] never admits.
693pub(in crate::blend) fn rails_on_shipped_windows(
694    rows: &FittedRows,
695    parameters: &[f64],
696    stations: &[Station],
697    carriers: [&NurbsSurface; 2],
698    windows: [Option<[f64; 2]>; 2],
699) -> (f64, [usize; 2]) {
700    let mut worst = 0.0_f64;
701    let mut samples = [0usize; 2];
702    let affine = rows.exact_extrusion && affine_reads_on();
703    for (side, (rail, carrier)) in [(&rows.cr, carriers[0]), (&rows.cs, carriers[1])].into_iter().enumerate() {
704        let Some([low, high]) = windows[side] else { continue };
705        // An exact extrusion's straight rail on a plane: the ends carry the
706        // worst (`affine_rail_standoff`), the samples between them cannot.
707        if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
708            crate::blend::record_count("count.read.affine_rails", 1);
709            samples[side] += 2;
710            if !worst.is_nan() && off > worst {
711                worst = off;
712            }
713            continue;
714        }
715        let mut at = vec![low, high];
716        for pair in parameters.windows(2) {
717            for fraction in [0.25, 0.5, 0.75] {
718                let u = pair[0] + (pair[1] - pair[0]) * fraction;
719                if u > low && u < high {
720                    at.push(u);
721                }
722            }
723        }
724        for u in at {
725            let off = interval_at(parameters, u)
726                .and_then(|(interval, fraction)| {
727                    let (left, right) = (&stations[interval], &stations[interval + 1]);
728                    let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
729                    let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
730                    let point = rail.evaluate(u).ok()?;
731                    crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
732                })
733                .filter(|off| off.is_finite())
734                .unwrap_or(f64::NAN);
735            samples[side] += 1;
736            if !worst.is_nan() && (off.is_nan() || off > worst) {
737                worst = off;
738            }
739        }
740    }
741    (worst, samples)
742}
743
744/// The JUNCTION stencil across the station between interval `left` and
745/// `left + 1`: `left_nodes` (33 exact centres of `left`) from node 16 to 32,
746/// then `right_nodes` (33 of `left + 1`) from node 1 to 16, the shared
747/// station at node 16. Its native abscissae come from `station_ts` (the
748/// edge parameters the centres were solved at): `None` when the two native
749/// widths are bitwise equal -- uniform nodes, read by the ordinary reader --
750/// else each node's t, interior nodes by the dense solves' own arithmetic
751/// t_i + (t_{i+1} − t_i)·j/32 and the shared station at its STORED t. Err
752/// when a station t is not finite or a width is not strictly positive.
753pub(in crate::blend) fn junction_stencil(
754    left_nodes: &[Vec3],
755    right_nodes: &[Vec3],
756    station_ts: &[f64],
757    left: usize,
758) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
759    let half = OPEN_WALL_CENTRES / 2;
760    if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
761        return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
762    }
763    let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
764    let (w_left, w_right) = (t1 - t0, t2 - t1);
765    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()) {
766        return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
767    }
768    let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
769    if w_left.to_bits() == w_right.to_bits() {
770        return Ok((nodes, None));
771    }
772    let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
773        .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
774        .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
775        .collect();
776    Ok((nodes, Some(abscissae)))
777}
778
779/// One wall probe on a stencil: the ordinary uniform reader when it has no
780/// native abscissae (an interval's own nodes, or a junction of equal native
781/// widths), the native-abscissa reader when it has them.
782pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
783    match abscissae {
784        None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
785        Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
786    }
787}
788
789/// Every interval's largest DECLARED wall reading (`chain::declared_wall_probe`,
790/// reading + uncertainty) on the open fillet `rows`, against the exact ball
791/// centres the march solved at j/32 of each interval ([`OpenDenseCentres`]),
792/// or, for each unread interval, WHY: its station
793/// interval and which component failed (no dense row, a dense row of the
794/// wrong size, an exact centre the march did not solve, a wall point the
795/// surface would not evaluate, a probe the declared reader declined, no probe
796/// in the shipped part, or a non-finite reading). The cause is diagnostic.
797///
798/// Read on the DECLARED SHIPPED REGION `window` only: an interval it overlaps
799/// is probed at its even stencil fractions that lie inside the window and at
800/// each window end inside the interval; a probe outside the window is not on
801/// the shipped wall and is not read. A window END whose nearest even stencil
802/// node falls outside 2..=30 -- an end within a node of the interval's own
803/// station, as the original bore's shipped spans were, 1e-16 from their
804/// vertex stations -- has its foot at the interval's end node, where
805/// [`crate::blend::chain::declared_wall_probe`] cannot read (the stencil
806/// cannot follow a foot out of its own interval). It is read on a JUNCTION
807/// stencil instead: the neighbouring interval's last half and this one's
808/// first half (or this one's last half and the next one's first), the same
809/// exact centres meeting at the shared station, with the probe at its
810/// middle node. Two intervals of unequal native width (local insertions)
811/// give the junction unequal node spacing, so it is then read by the
812/// native-abscissa reader (`chain::declared_wall_probe_at_why`) on the
813/// edge parameters the centres were solved at; of equal width, by the
814/// ordinary reader. With no neighbour it is read as before.
815/// The wall reading of an EXACT extrusion's rows, read directly: the wall is
816/// the ball's sweep along the centre line, so every point of the shipped
817/// surface stands exactly the radius off that line, and the reading is how
818/// far it does not. One entry per interval overlapping `window`, probes at
819/// the interval's quarters inside the window and at the window's ends, over
820/// the section at k/8 (the ball-resolve reader's sections). Never for fitted
821/// rows: those are read against the exact centres between their stations
822/// ([`open_wall_readings_why`]).
823pub(in crate::blend) fn exact_extrusion_wall_readings(
824    rows: &FittedRows,
825    parameters: &[f64],
826    window: [f64; 2],
827    radius: f64,
828) -> Vec<Result<f64, String>> {
829    let intervals = parameters.len().saturating_sub(1);
830    let overlapped: Vec<usize> =
831        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
832    let line = match (rows.exact_extrusion, rows.center.as_ref()) {
833        (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
834        (false, _) => Err("the rows are not an exact extrusion".to_string()),
835        (true, None) => Err("the exact rows carry no centre line".to_string()),
836    };
837    let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
838        Ok(line) => line,
839        Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
840    };
841    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
842        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
843    };
844    overlapped
845        .into_iter()
846        .map(|interval| -> Result<f64, String> {
847            let (from, to) = (parameters[interval], parameters[interval + 1]);
848            let inside = |u: f64| u >= window[0] && u <= window[1];
849            let mut probes: Vec<f64> =
850                (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
851            probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
852            if probes.is_empty() {
853                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
854            }
855            let mut worst = 0.0_f64;
856            for u in probes {
857                for k in 1..OPEN_WALL_SECTIONS {
858                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
859                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
860                    let offset = point.sub(anchor);
861                    let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
862                    let reading = (standoff - radius).abs();
863                    if !reading.is_finite() {
864                        return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
865                    }
866                    worst = worst.max(reading);
867                }
868            }
869            Ok(worst)
870        })
871        .collect()
872}
873
874/// The wall reading of an EXACT REVOLUTION's rows
875/// (`support::exact_revolution_rows`), read directly: the wall is the ball's
876/// sweep along the centre CIRCLE, so every point of the shipped surface
877/// stands exactly the radius off that circle, and the reading is how far it
878/// does not. The distance to the circle is closed-form in the point's
879/// meridian half-plane: `hypot(ρ − R_c, h − h_c)` for the point's radial
880/// distance ρ and axial height h about the rows' axis. Same probes as
881/// [`exact_extrusion_wall_readings`]: one entry per interval overlapping
882/// `window`, the interval's quarters inside the window and the window's
883/// ends, over the section at k/8.
884pub(in crate::blend) fn exact_revolution_wall_readings(
885    rows: &FittedRows,
886    parameters: &[f64],
887    window: [f64; 2],
888    radius: f64,
889) -> Vec<Result<f64, String>> {
890    let intervals = parameters.len().saturating_sub(1);
891    let overlapped: Vec<usize> =
892        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
893    let circle = match (rows.revolution_axis, rows.center.as_ref()) {
894        (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
895        (None, _) => Err("the rows are not an exact revolution".to_string()),
896        (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
897    };
898    let (origin, axis, centre_radius, centre_height) = match circle {
899        Ok((origin, axis, point)) => {
900            let offset = point.sub(origin);
901            let height = offset.dot(axis);
902            (origin, axis, offset.sub(axis.scale(height)).length(), height)
903        }
904        Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
905    };
906    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
907        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
908    };
909    overlapped
910        .into_iter()
911        .map(|interval| -> Result<f64, String> {
912            let (from, to) = (parameters[interval], parameters[interval + 1]);
913            let inside = |u: f64| u >= window[0] && u <= window[1];
914            let mut probes: Vec<f64> =
915                (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
916            probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
917            if probes.is_empty() {
918                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
919            }
920            let mut worst = 0.0_f64;
921            for u in probes {
922                for k in 1..OPEN_WALL_SECTIONS {
923                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
924                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
925                    let offset = point.sub(origin);
926                    let height = offset.dot(axis);
927                    let radial = offset.sub(axis.scale(height)).length();
928                    let standoff = (radial - centre_radius).hypot(height - centre_height);
929                    let reading = (standoff - radius).abs();
930                    if !reading.is_finite() {
931                        return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
932                    }
933                    worst = worst.max(reading);
934                }
935            }
936            Ok(worst)
937        })
938        .collect()
939}
940
941pub(in crate::blend) fn open_wall_readings_why(
942    rows: &FittedRows,
943    parameters: &[f64],
944    stations: &[Station],
945    window: [f64; 2],
946    dense: &OpenDenseCentres,
947    radius: f64,
948) -> Vec<Result<f64, String>> {
949    let intervals = parameters.len().saturating_sub(1);
950    let overlapped: Vec<usize> =
951        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
952    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
953        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
954    };
955    // The exact centres of one interval at j/32, its stations at both ends.
956    let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
957        let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
958        if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
959            return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
960        }
961        let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
962        centres.push(stations[interval].center);
963        for (j, centre) in inner.iter().enumerate() {
964            centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
965        }
966        centres.push(stations[interval + 1].center);
967        Ok(centres)
968    };
969    let half = OPEN_WALL_CENTRES / 2;
970    // The junction stencil across the station between `left` and `left + 1`:
971    // left's nodes half..=32, then right's nodes 1..=half; the station is node
972    // `half`. With each node's NATIVE abscissa (the edge parameter its exact
973    // centre was solved at, `OpenDenseCentres::station_ts`), and `None` when
974    // the two intervals' native widths are bitwise equal: the nodes are then
975    // uniform and the ordinary reader reads them, bit for bit.
976    let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
977        if dense.station_ts.len() != parameters.len() {
978            return Err(format!("interval {left}: junction without the native station parameters"));
979        }
980        junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
981    };
982    overlapped
983        .into_iter()
984        .map(|interval| -> Result<f64, String> {
985            let centres = nodes_of(interval)?;
986            let (from, to) = (parameters[interval], parameters[interval + 1]);
987            let inside = |u: f64| u >= window[0] && u <= window[1];
988            // (u, stencil node, the stencil: None = this interval's own;
989            // a junction's nodes with its native abscissae when unequal).
990            let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
991                .step_by(2)
992                .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
993                .filter(|(u, _)| inside(*u))
994                .map(|(u, step)| (u, step, None))
995                .collect();
996            for end in window {
997                if end > from && end < to {
998                    let fraction = (end - from) / (to - from);
999                    let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
1000                    #[cfg(not(test))]
1001                    let junction_off = false;
1002                    if junction_off {
1003                        probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1004                    } else if nearest < 2 && interval > 0 {
1005                        probes.push((end, half, Some(junction(interval - 1)?)));
1006                    } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
1007                        probes.push((end, half, Some(junction(interval)?)));
1008                    } else {
1009                        probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1010                    }
1011                }
1012            }
1013            if probes.is_empty() {
1014                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1015            }
1016            let mut declared = 0.0_f64;
1017            for (u, step, stencil) in probes {
1018                let (nodes, abscissae, what) = match &stencil {
1019                    Some((nodes, None)) => (nodes, None, "junction"),
1020                    Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
1021                    None => (&centres, None, "interval"),
1022                };
1023                for k in 1..OPEN_WALL_SECTIONS {
1024                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1025                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1026                    // The interval's own stencil reads from the probe's node,
1027                    // then (declined) from where its fraction reaches along the
1028                    // centre path (a stationary section); a junction keeps its
1029                    // own dispatch.
1030                    let read = if stencil.is_none() {
1031                        crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
1032                    } else {
1033                        read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
1034                    };
1035                    let (reading, uncertainty) = read.map_err(|why| {
1036                        format!(
1037                            "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
1038                             (u {u:.17}): {why}"
1039                        )
1040                    })?;
1041                    declared = declared.max(reading + uncertainty);
1042                }
1043            }
1044            if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
1045        })
1046        .collect()
1047}
1048
1049
1050/// A fresh id allocator seeded past the vertex, edge, face, loop and coedge IDs.
1051/// Solid and shell IDs are outside this allocation domain. Shared by
1052/// a whole group of stripes so their vertices, edges and coedges cannot
1053/// collide.
1054pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
1055    let mut next_id = solid
1056        .vertices
1057        .iter()
1058        .map(|vertex| vertex.id)
1059        .chain(solid.edges.iter().map(|edge| edge.id))
1060        .chain(
1061            solid
1062                .shells
1063                .iter()
1064                .flat_map(|shell| &shell.faces)
1065                .flat_map(|face| {
1066                    face.loops
1067                        .iter()
1068                        .map(|loop_record| loop_record.id)
1069                        .chain(face.loops.iter().flat_map(|loop_record| {
1070                            loop_record.coedges.iter().map(|coedge| coedge.id)
1071                        }))
1072                        .chain(std::iter::once(face.id))
1073                }),
1074        )
1075        .max()
1076        .unwrap_or(0)
1077        + 1;
1078    move || {
1079        let id = next_id;
1080        next_id += 1;
1081        id
1082    }
1083}
1084
1085
1086/// May a declined support refit fall back on the march's own trim pcurve?
1087/// Only when that trim, read independently as shipped
1088/// (`read_shipped_pcurve`: miss, rail standoff, samples, worst), meets the
1089/// floor ON its rail's branch: its miss PLUS the rail's standoff from the
1090/// carrier at the trim's foot within `floor`. A trim on another branch of the
1091/// carrier tracks its own foot perfectly (miss 0) while that foot stands far
1092/// off the rail, so a miss alone would ship it; an unreadable trim never
1093/// stands.
1094pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
1095    matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
1096}
1097
1098/// Slice 1b of the surgery lane (`exact_trim_on`): the support trim of a
1099/// straight rail on a PLANE, exact. The feet of the rail's two ends on the
1100/// plane (the kernel's closed-form projection) joined by a degree-1 pcurve
1101/// on the rail's own parameter range, so the surgery reads it at the rail's
1102/// parameter as it reads every trim; read once as shipped
1103/// (`read_shipped_pcurve`) and kept only when it stands within the pcurve
1104/// floor by the march trim's own rule (`march_trim_stands`: miss plus rail
1105/// standoff). `None` hands the trim to the refit road, as a march trim that
1106/// does not stand would be.
1107fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
1108    if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
1109        return Ok(None);
1110    }
1111    let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1112    let mut control = Vec::with_capacity(2);
1113    for t in [d0, d1] {
1114        let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1115        let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1116        control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1117    }
1118    let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1119    feet_timing.stop();
1120    let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1121    let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
1122        Some(read) => {
1123            crate::blend::record_count("count.read.affine_trims", 1);
1124            Ok(read)
1125        }
1126        None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
1127    };
1128    read_timing.stop();
1129    Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1130}
1131
1132/// [`read_shipped_pcurve`](crate::blend::track_fit::read_shipped_pcurve)'s
1133/// `(miss, standoff, samples, worst)` for a degree-1 `pcurve` on an AFFINE
1134/// plane against an affine rail, read at `d0` and `d1` only: the pcurve's
1135/// image, the rail and the rail's foot on the plane are all affine in the
1136/// rail parameter, so the miss (image to foot) and the standoff (rail to foot)
1137/// are norms of affine functions, convex, and largest at an end. `None` when
1138/// any of the three is not affine, or an end will not evaluate.
1139fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
1140    if !affine_curve(rail) || !affine_curve(pcurve) {
1141        return None;
1142    }
1143    let (origin, normal) = affine_plane(surface)?;
1144    let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
1145    for t in [d0, d1] {
1146        let point = rail.evaluate(t).ok()?;
1147        let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
1148        let uv = pcurve.evaluate(t).ok()?;
1149        let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
1150        let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
1151        if !(residual.is_finite() && off.is_finite()) {
1152            return None;
1153        }
1154        if residual > miss {
1155            (miss, worst) = (residual, t);
1156        }
1157        standoff = standoff.max(off);
1158    }
1159    Some((miss, standoff, 2, worst))
1160}
1161
1162/// Sew ONE stripe into `result`.
1163///
1164/// `solid` is the ORIGINAL topology every stripe was marched against (the
1165/// row-coincidence detection reads it); `result` is the shared evolving
1166/// solid, and `take_id` the shared id allocator — a group of stripes meeting
1167/// at a corner must create their rim vertices and cross-section arcs from one
1168/// counter, and must see each other's work.
1169///
1170/// Each end is either free (terminate on the face across the corner) or a
1171/// corner stop; see [`EndPlan`].  Pruning orphaned vertices is the caller's
1172/// job, once, after every stripe of the group is in.
1173pub(in crate::blend) fn build_open_surgery(
1174    solid: &BrepSolid,
1175    result: &mut BrepSolid,
1176    take_id: &mut dyn FnMut() -> u64,
1177    edge: &EdgeRecord,
1178    first: &BlendMate,
1179    second: &BlendMate,
1180    rows: FittedRows,
1181    ends: [EndPlan; 2],
1182    name: Option<&str>,
1183) -> Result<SewnStripe, KernelRefusal> {
1184    let [start_end, finish_end] = ends;
1185
1186    // Trim the support rows to the crossing window.
1187    // A POLE end stops EXACTLY on the row's own end (the march puts the
1188    // pole's station there and overshoots no further), so there is nothing to
1189    // split off on that side.  Only equality: a stop PAST the row's end — a
1190    // free end's crossing the rows never reached — still fails the split and
1191    // refuses, rather than building a rail out to wherever the rows stop.
1192    let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1193        let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1194        let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1195        if b == high {
1196            return Ok(tail);
1197        }
1198        let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1199        Ok(middle)
1200    };
1201    let kind = |plan: &EndPlan| match plan {
1202        EndPlan::Free(_) => "free",
1203        EndPlan::Corner(_) => "corner",
1204        EndPlan::Miter(_) => "miter",
1205        EndPlan::Cap(_) => "cap",
1206    };
1207    let describe_trim = |error: KernelRefusal| {
1208        error.with_message(|error| {
1209            format!(
1210                "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1211                 at cr {:.6}/cs {:.6})",
1212                edge.id,
1213                kind(&start_end),
1214                start_end.cr_parameter(),
1215                start_end.cs_parameter(),
1216                kind(&finish_end),
1217                finish_end.cr_parameter(),
1218                finish_end.cs_parameter()
1219            )
1220        })
1221    };
1222    // A rail whose two stops are one point within `consumed_band` has no strip
1223    // to trim: the blend takes that mate down to a single point along this
1224    // edge.  That is a FULL-WIDTH corner — the shared face of a miter whose
1225    // radius is the side face's width, every face of a star at full width, the
1226    // middle edge of a channel whose two corner setbacks meet.  No rail edge is
1227    // built; the mate loses the blended edge instead of having it replaced, and
1228    // the rail's two rim vertices are one vertex, which the orchestrator
1229    // identifies (`SewnStripe::identified`) once every stripe is in.  A stripe
1230    // free at BOTH ends has no corner to collapse onto, so it refuses.
1231    let band = consumed_band(solid);
1232    let exact_trim = crate::blend::exact_trim_on();
1233    let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1234        let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1235        Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1236            && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1237    };
1238    let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1239    let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1240    if cr_collapsed || cs_collapsed {
1241        let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1242            Some("neither end is a corner")
1243        } else if first.face.id == second.face.id {
1244            Some("both rails lie on one face")
1245        } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1246            Some("a capped end has no corner to share the point with")
1247        } else {
1248            None
1249        };
1250        if let Some(reason) = unsupported {
1251            return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1252                "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1253                edge.id
1254            )));
1255        }
1256    }
1257    // The support pcurve the rows carry interpolates the stations' (u, v) and
1258    // is read nowhere between them: on the 20-degree crossing's notched open
1259    // exit arc it stood 2.98e-5 off the rail it trims (q1 = 9cd8c3163 + the
1260    // section-fit fix, read against the exact cylinders). Inside the crossing
1261    // window — on the face, never on a carrier's extension — it is refitted
1262    // to the trimmed rail's own projected track at the pcurve floor and read
1263    // again as shipped (`refit_closed_support_pcurve`, the closed edge's).
1264    // A rail that overruns a PLANE carrier's chart (the rib-base fin top's
1265    // rails run 0.303 past it) is on the plane's extension, which the chart
1266    // clamps: there the refit and both reads would measure the clamp, and the
1267    // trim is instead rebuilt from its edge on the widened chart and verified
1268    // at the floor by `fit_planar_charts_to_trims`, which every lane that
1269    // sews these stripes runs.
1270    let trim_rail = |collapsed: bool,
1271                     row: &NurbsCurve,
1272                     pcurve: &NurbsCurve,
1273                     surface: &NurbsSurface,
1274                     a: f64,
1275                     b: f64|
1276     -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1277        if collapsed {
1278            return Ok(None);
1279        }
1280        let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1281        let rail = trim_row(row, a, b).map_err(describe_trim)?;
1282        let interpolated = trim_row(pcurve, a, b)?;
1283        rows_timing.stop();
1284        {
1285            let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1286            let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1287            let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1288            overrun_timing.stop();
1289            if overrun.is_some_and(|excursion| excursion > band) {
1290                return Ok(Some((rail, interpolated)));
1291            }
1292        }
1293        let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1294        // Slice 1b (`exact_trim_on`): an exact extrusion row's rail on a
1295        // plane ships the exact line in uv, read once; the refit road below
1296        // is for what it declines.
1297        if exact_trim && rows.exact_extrusion {
1298            if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1299                crate::blend::record_count("count.surgery.exact_trim", 1);
1300                return Ok(Some((rail, line)));
1301            }
1302        }
1303        // The exact REVOLUTION rows' own trims are exact by construction
1304        // (the carrier's uv line, the plane's affine image of the rail):
1305        // read once as shipped by the march trim's rule (`march_trim_stands`)
1306        // and kept; whatever does not stand takes the refit road below.
1307        if rows.revolution_axis.is_some() {
1308            let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1309            let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1310            read_timing.stop();
1311            if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1312                crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1313                return Ok(Some((rail, interpolated)));
1314            }
1315            crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1316        }
1317        // The march's OWN trim pcurve, read independently at the rail's own
1318        // parameter against the same floor (`read_shipped_pcurve`): what a
1319        // declined refit may fall back to, and the standoff a refit on the
1320        // rail's own branch must match.
1321        // The feet of each pcurve against the rail are sought once and
1322        // shared between the reads below (`FootMemo`): the march trim's for
1323        // its own read and the branch comparison, the refit's for the
1324        // refit's own read, the branch comparison and the absolute bound.
1325        let mut march_feet = crate::blend::track_fit::FootMemo::default();
1326        let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1327        let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1328        let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1329        march_read_timing.stop();
1330        let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1331        let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1332            // Kept ONLY when the march's trim stands on its own read
1333            // (`march_trim_stands`); otherwise the refusal stands.
1334            let stands = march_trim_stands(&march_read, floor);
1335            if stands {
1336                Ok(Some((rail.clone(), interpolated.clone())))
1337            } else {
1338                Err(why)
1339            }
1340        };
1341        let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1342        let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1343        refit_timing.stop();
1344        let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1345        match refit {
1346            Ok((fitted, refit_read)) => {
1347                // A refit tracks ITS OWN projected track; a track that took
1348                // another branch of the carrier tracks itself perfectly while
1349                // its foot stands far off the rail (the stationary-start
1350                // prism's coedge read 6.0 off its edge). Its foot standoff
1351                // must be the rail's own standoff on the march trim's branch.
1352                // BOTH reads are required: an unreadable refit read declines
1353                // the refit (the march trim then stands only on its own read
1354                // within the floor), and an unreadable march read cannot
1355                // establish that the refit is on the rail's branch, so it
1356                // refuses. Never shipped on an unknown.
1357                // The two are compared POINTWISE, at the same rail
1358                // parameters (`standoff_excess`), never as two maxima read at
1359                // each curve's own knots.
1360                // The refit's read is the shipped curve's own, handed back by
1361                // the refit (the same `read_shipped_pcurve` on the same
1362                // inputs, once).
1363                let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1364                    (_, Err(error)) => return declined(error),
1365                    (Err(error), Ok(_)) => {
1366                        return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1367                            "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1368                             trim is unreadable ({})",
1369                            error.message
1370                        )));
1371                    }
1372                    (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1373                };
1374                let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1375                let excess = crate::blend::track_fit::standoff_excess_memo(
1376                    surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1377                );
1378                excess_timing.stop();
1379                let excess = match excess {
1380                    Ok(excess) => excess,
1381                    Err(error) => return declined(error),
1382                };
1383                if !(excess <= floor) {
1384                    return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1385                        "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1386                         at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1387                         another branch of the carrier"
1388                    )));
1389                }
1390                // ABSOLUTE: the comparison above is relative to the march
1391                // trim, so a refit that follows an off-branch march trim
1392                // passes it. The refit's foot must also stand within the floor
1393                // of the rail's own deviation from its carrier, read by the
1394                // nearest-point projector at the same parameters
1395                // (`standoff_over_nearest`) -- the rail's approximation the
1396                // lane reports, never more.
1397                let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1398                let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1399                nearest_timing.stop();
1400                let absolute = match absolute {
1401                    Ok(absolute) => absolute,
1402                    Err(error) => return declined(error),
1403                };
1404                if !(absolute <= floor) {
1405                    return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1406                        "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1407                         off its carrier: its foot is not the rail's nearest point"
1408                    )));
1409                }
1410                Ok(Some((rail, fitted)))
1411            }
1412            Err(error) => declined(error),
1413        }
1414    };
1415    let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1416    let cr_rail = trim_rail(
1417        cr_collapsed,
1418        &rows.cr,
1419        &rows.cr_pcurve,
1420        &first.face.surface,
1421        start_end.cr_parameter(),
1422        finish_end.cr_parameter(),
1423    )?;
1424    let cs_rail = trim_rail(
1425        cs_collapsed,
1426        &rows.cs,
1427        &rows.cs_pcurve,
1428        &second.face.surface,
1429        start_end.cs_parameter(),
1430        finish_end.cs_parameter(),
1431    )?;
1432    trim_timing.stop();
1433    // A collapsed rail's domain is its two stops, which are one point.
1434    let cr_domain = match &cr_rail {
1435        Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1436        None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1437    };
1438    let cs_domain = match &cs_rail {
1439        Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1440        None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1441    };
1442    // A rail's end points, read off the trimmed rail or, collapsed, the row.
1443    let cr_at = |parameter: f64| match &cr_rail {
1444        Some((cr, _)) => cr.evaluate(parameter),
1445        None => rows.cr.evaluate(parameter),
1446    };
1447    let cs_at = |parameter: f64| match &cs_rail {
1448        Some((cs, _)) => cs.evaluate(parameter),
1449        None => rows.cs.evaluate(parameter),
1450    };
1451    let old_start = edge.start_vertex_id;
1452    let old_finish = edge.end_vertex_id;
1453
1454    // Classify each of the four support crossings.  A crossing that lands at
1455    // the FAR endpoint of the boundary edge it meets (the end away from the
1456    // corner) means a prior fillet already consumed that whole boundary: the
1457    // new blend reuses the existing vertex there and the boundary edge is
1458    // deleted (its role is taken over by the new transverse curve on the
1459    // prior blend face).  An interior crossing is the pristine case — a fresh
1460    // vertex with the boundary trimmed to it.
1461    //
1462    // Consumed means within `consumed_band` — the slack `check_support_extent`
1463    // refuses past, so a rail between the two is not possible.
1464    let classify =
1465        |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1466            let boundary = result
1467                .edges
1468                .iter()
1469                .find(|candidate| candidate.id == boundary_id)
1470                .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1471            let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1472            let far_vertex = if boundary.start_vertex_id == corner {
1473                Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1474            } else if boundary.end_vertex_id == corner {
1475                Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1476            } else {
1477                None
1478            };
1479            if let Some((far_id, far_t, near_t)) = far_vertex {
1480                // A consumed boundary is a COINCIDENCE — a prior fillet's rim
1481                // vertex is exactly where this rail crosses — and the operands
1482                // were healed before the march, so it holds to solver
1483                // precision.  It is not "near the far end": a fillet whose
1484                // radius is 1e-3 short of the face's width crosses 1e-3 from
1485                // the far vertex and must keep that sliver, or the face is
1486                // snapped a full 1e-3 out of true (the oversized-radius limit
1487                // cases and issue 1177 measure exactly that).  Measured in
1488                // model units, never in the boundary's parameter — a unit
1489                // parameter on a 20-long edge would call 2e-3 a coincidence.
1490                let far_point = result
1491                    .vertices
1492                    .iter()
1493                    .find(|candidate| candidate.id == far_id)
1494                    .map(|candidate| candidate.point)
1495                    .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1496                let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1497                let _ = span;
1498                let past_far = crossing.sub(far_point).length();
1499                if past_far <= band {
1500                    // Sanity: the reused vertex must exist.
1501                    if !result
1502                        .vertices
1503                        .iter()
1504                        .any(|candidate| candidate.id == far_id)
1505                    {
1506                        return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1507                    }
1508                    return Ok(RimResolution::Consumed(far_id));
1509                }
1510                // A crossing BEYOND the far vertex is a rail that runs off
1511                // its face: the blend is wider than the face by more than the
1512                // band a snap may close, and trimming the boundary there would
1513                // extend it into air.  `check_support_extent` refuses this
1514                // before the march on every face the exact tool understands;
1515                // this is the refusal for the rest.  It is not the whole
1516                // answer: a crossing the solve CLAMPS to the far vertex reads
1517                // as consumed here, and `detect_row_coincidence` is what sees
1518                // that row leave the face.  Measured on the census: it fires on
1519                // a wall wider than its closed chain's arc (0.5 past) and on the
1520                // r = 10.001 pinch past an earlier round (2e-3 past), both of
1521                // which refused later in the surgery before.
1522                if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1523                    return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1524                        "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1525                         past that edge's far vertex, and a rail within {band:.3e} of it is the \
1526                         most this surgery snaps onto the edge"
1527                    )));
1528                }
1529            }
1530            Ok(RimResolution::Fresh)
1531        };
1532    // A CORNER end has no boundary to classify: the ball's tangency vertex is
1533    // the rim, and the boundary edge that used to reach the sharp corner is
1534    // the neighbouring stripe's own blended edge, which that stripe replaces.
1535    let resolve_side = |plan: &EndPlan,
1536                        side_first: bool,
1537                        corner: u64|
1538     -> Result<RimResolution, KernelRefusal> {
1539        if let Some(vertex) = plan.planned_rim(side_first) {
1540            return Ok(RimResolution::Corner(vertex));
1541        }
1542        match plan {
1543            EndPlan::Free(free) => classify(
1544                if side_first {
1545                    free.first_edge_id
1546                } else {
1547                    free.second_edge_id
1548                },
1549                if side_first {
1550                    free.first_edge_parameter
1551                } else {
1552                    free.second_edge_parameter
1553                },
1554                corner,
1555            ),
1556            EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1557                Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1558            }
1559        }
1560    };
1561    let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1562    let start_first = resolve_side(&start_end, true, old_start)?;
1563    let start_second = resolve_side(&start_end, false, old_start)?;
1564    let finish_first = resolve_side(&finish_end, true, old_finish)?;
1565    let finish_second = resolve_side(&finish_end, false, old_finish)?;
1566    crossings_timing.stop();
1567    let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1568
1569    // Manifold pairing: the blend's use of the first support row must oppose
1570    // F1's use of the blended edge (see the blend-loop construction below).
1571    let first_use_forward = first.coedge.forward;
1572    let blend_cr_forward = !first_use_forward;
1573
1574    // Curve-level coincidence (OCCT PR #1449 lesson 3): when BOTH of a mate's
1575    // crossings are endpoint-consumed AND the trimmed support row retraces the
1576    // existing edge joining the two far vertices, that mate face is FULLY
1577    // consumed — the surgery sews the blend face straight onto the existing
1578    // edge and drops the zero-width face, instead of creating a coincident
1579    // fresh support edge that would leave a sliver strip (the r = face-width
1580    // class).  Each side is detected independently; any ambiguity keeps the
1581    // pristine path (fail-safe).  `row_traversed_from_start`: the blend loop
1582    // walks cr in station order iff `blend_cr_forward`, and cs in the
1583    // OPPOSITE order (see the two loop branches below).
1584    // A stripe with a CORNER end has no boundary edges at that end to be
1585    // coincident with, so the detection only runs on stripes that are free at
1586    // both ends.  (Fail-safe: not detecting a coincidence keeps the pristine
1587    // path, which is what a corner stop wants anyway.)
1588    let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1589        (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1590            detect_row_coincidence(
1591                solid,
1592                first,
1593                edge.id,
1594                start_free.first_edge_id,
1595                finish_free.first_edge_id,
1596                start_first.is_consumed(),
1597                start_first.existing().unwrap_or(0),
1598                finish_first.is_consumed(),
1599                finish_first.existing().unwrap_or(0),
1600                cr,
1601                blend_cr_forward,
1602                band,
1603            )?,
1604            detect_row_coincidence(
1605                solid,
1606                second,
1607                edge.id,
1608                start_free.second_edge_id,
1609                finish_free.second_edge_id,
1610                start_second.is_consumed(),
1611                start_second.existing().unwrap_or(0),
1612                finish_second.is_consumed(),
1613                finish_second.existing().unwrap_or(0),
1614                cs,
1615                !blend_cr_forward,
1616                band,
1617            )?,
1618        ),
1619        _ => (None, None),
1620    };
1621    // One face hosting both sides (the blended edge used twice by one face)
1622    // cannot be dropped for one side while the other still splices into it —
1623    // ambiguous, keep the pristine path for both.
1624    let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1625        if first.face.id == second.face.id {
1626            (None, None)
1627        } else {
1628            (sew_first, sew_second)
1629        };
1630
1631    // Resolve the four rim vertices: reuse the existing vertex for a consumed
1632    // side, else create a fresh vertex at the support-row endpoint.
1633    let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1634        match rim.existing() {
1635            Some(id) => Ok(id),
1636            None => {
1637                let id = take_id();
1638                result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1639                Ok(id)
1640            }
1641        }
1642    };
1643    let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1644    let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1645    let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1646    let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1647    // How far the consumed rims and sewn rows moved a rail onto what already
1648    // existed — what `check_snap_closure` measures the body against.
1649    let mut snap = [&sew_first, &sew_second]
1650        .into_iter()
1651        .flatten()
1652        .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1653    for (rim, vertex, row_end) in [
1654        (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1655        (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1656        (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1657        (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1658    ] {
1659        if !rim.is_consumed() {
1660            continue;
1661        }
1662        let point = result
1663            .vertices
1664            .iter()
1665            .find(|candidate| candidate.id == vertex)
1666            .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1667            .point;
1668        snap = snap.max(row_end.sub(point).length());
1669    }
1670
1671    // Support edges: a sewn side reuses the EXISTING coincident edge (no
1672    // fresh edge — OCCT's `SetExistingEdge` move); a pristine side gets the
1673    // fitted row as a fresh edge.
1674    // A collapsed side builds no edge at all.
1675    let cr_edge_id = match (&sew_first, &cr_rail) {
1676        (Some(sew), _) => Some(sew.edge_id),
1677        (None, None) => None,
1678        (None, Some((cr, _))) => {
1679            let id = take_id();
1680            result.edges.push(EdgeRecord {
1681                id,
1682                curve: cr.clone(),
1683                t0: cr_domain[0],
1684                t1: cr_domain[1],
1685                start_vertex_id: w1a,
1686                end_vertex_id: w1b,
1687                degenerate: false,
1688                name: None,
1689            });
1690            Some(id)
1691        }
1692    };
1693    let cs_edge_id = match (&sew_second, &cs_rail) {
1694        (Some(sew), _) => Some(sew.edge_id),
1695        (None, None) => None,
1696        (None, Some((cs, _))) => {
1697            let id = take_id();
1698            result.edges.push(EdgeRecord {
1699                id,
1700                curve: cs.clone(),
1701                t0: cs_domain[0],
1702                t1: cs_domain[1],
1703                start_vertex_id: w2a,
1704                end_vertex_id: w2b,
1705                degenerate: false,
1706                name: None,
1707            });
1708            Some(id)
1709        }
1710    };
1711    // The edge closing each end of the blend face: the §6.9 transverse curve
1712    // on the end face for a free end, or — at a corner — the end
1713    // cross-section arc the corner patch was already given, committed by the
1714    // caller and merely referenced here.
1715    let mut commit_end_edge = |plan: &EndPlan,
1716                               first_rim: u64,
1717                               second_rim: u64|
1718     -> Result<Option<u64>, KernelRefusal> {
1719        match plan {
1720            EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1721            EndPlan::Free(free) => {
1722                let id = take_id();
1723                let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1724                result.edges.push(EdgeRecord {
1725                    id,
1726                    curve: free.transverse_curve.clone(),
1727                    t0: domain[0],
1728                    t1: domain[1],
1729                    start_vertex_id: first_rim,
1730                    end_vertex_id: second_rim,
1731                    degenerate: false,
1732                    name: None,
1733                });
1734                Ok(Some(id))
1735            }
1736        }
1737    };
1738    let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
1739    let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
1740
1741    // Replace the blended edge in each PRISTINE mate's loop — the blend
1742    // face's loop direction is forced by manifold pairing with F1's use of
1743    // the blended edge (`first_use_forward`, captured above).  A sewn mate is
1744    // dropped whole below; nothing to splice.  A COLLAPSED side has no rail to
1745    // put there: the mate simply loses the blended edge, and its loop closes
1746    // through the rim vertices the orchestrator identifies.
1747    for (mate, rail, sewn) in [
1748        (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1749        (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1750    ] {
1751        if sewn {
1752            continue;
1753        }
1754        let face = result
1755            .shells
1756            .iter_mut()
1757            .flat_map(|shell| &mut shell.faces)
1758            .find(|face| face.id == mate.face.id)
1759            .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1760        let loop_record = &mut face.loops[mate.loop_index];
1761        let position = loop_record
1762            .coedges
1763            .iter()
1764            .position(|coedge| coedge.edge_id == edge.id)
1765            .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1766        let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1767            loop_record.coedges.remove(position);
1768            continue;
1769        };
1770        let old_forward = loop_record.coedges[position].forward;
1771        loop_record.coedges[position] = CoedgeRecord {
1772            id: loop_record.coedges[position].id,
1773            edge_id: new_edge_id,
1774            forward: old_forward,
1775            pcurve: if old_forward {
1776                pcurve_forward.clone()
1777            } else {
1778                pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1779            },
1780        };
1781    }
1782
1783    // A capped end's legs: on each mate, the leg runs from the rail's rim
1784    // vertex to the sharp vertex, where the continuing edge still starts.
1785    // It goes between the rail coedge and that continuing coedge, with the
1786    // sense that walks rim -> vertex when the rail ends at the rim.
1787    for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
1788        let EndPlan::Cap(cap) = plan else {
1789            continue;
1790        };
1791        for (mate, rail_edge_id, leg, rim) in [
1792            (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
1793            (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
1794        ] {
1795            // Refused above: a capped end never meets a collapsed rail.
1796            let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
1797            let face = result
1798                .shells
1799                .iter_mut()
1800                .flat_map(|shell| &mut shell.faces)
1801                .find(|face| face.id == mate.face.id)
1802                .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
1803            let loop_record = &mut face.loops[mate.loop_index];
1804            let count = loop_record.coedges.len();
1805            let rail_at = loop_record
1806                .coedges
1807                .iter()
1808                .position(|coedge| coedge.edge_id == rail_edge_id)
1809                .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
1810            // Which side of the rail coedge is this end?  The rail coedge
1811            // traverses rim-to-rim; the capped end is at its traversal END
1812            // when walking forward means station order and this is the
1813            // finish end, etc.  Decide by geometry: the neighbour whose
1814            // traversal touches the sharp vertex.
1815            let rail_forward = loop_record.coedges[rail_at].forward;
1816            let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
1817            let (insert_at, forward, pcurve) = if end_is_traversal_end {
1818                // rail ... rim -> [leg rim->vertex] -> continuing edge
1819                (rail_at + 1, true, leg.1.clone())
1820            } else {
1821                // continuing edge -> [leg vertex->rim] -> rim ... rail
1822                (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
1823            };
1824            let _ = (count, rim);
1825            loop_record.coedges.insert(
1826                insert_at,
1827                CoedgeRecord {
1828                    id: take_id(),
1829                    edge_id: leg.0,
1830                    forward,
1831                    pcurve,
1832                },
1833            );
1834        }
1835    }
1836
1837    // Drop consumed boundary edges from the mate loop they share with the
1838    // blended edge: the fresh support coedge already begins at the reused far
1839    // vertex, so removing the fully-covered boundary keeps the loop closed.
1840    // A SEWN mate's whole loop collapses (the face is dropped below), so its
1841    // consumed boundaries are only recorded for deletion, never spliced.
1842    let mut consumed_edges: Vec<u64> = Vec::new();
1843    let start_free = start_end.free();
1844    let finish_free = finish_end.free();
1845    for (mate, sewn, sides) in [
1846        (
1847            first,
1848            sew_first.is_some(),
1849            [
1850                (&start_first, start_free.map(|free| free.first_edge_id)),
1851                (&finish_first, finish_free.map(|free| free.first_edge_id)),
1852            ],
1853        ),
1854        (
1855            second,
1856            sew_second.is_some(),
1857            [
1858                (&start_second, start_free.map(|free| free.second_edge_id)),
1859                (&finish_second, finish_free.map(|free| free.second_edge_id)),
1860            ],
1861        ),
1862    ] {
1863        for (cross, boundary_id) in sides {
1864            if !cross.is_consumed() {
1865                continue;
1866            }
1867            let Some(boundary_id) = boundary_id else {
1868                continue;
1869            };
1870            if !sewn {
1871                let face = result
1872                    .shells
1873                    .iter_mut()
1874                    .flat_map(|shell| &mut shell.faces)
1875                    .find(|face| face.id == mate.face.id)
1876                    .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1877                face.loops[mate.loop_index]
1878                    .coedges
1879                    .retain(|coedge| coedge.edge_id != boundary_id);
1880            }
1881            if !consumed_edges.contains(&boundary_id) {
1882                consumed_edges.push(boundary_id);
1883            }
1884        }
1885    }
1886    // Lesson 7: zero-span leftovers of a collapsing loop go with their face.
1887    for sew in [&sew_first, &sew_second].into_iter().flatten() {
1888        for id in &sew.collapsed_edges {
1889            if !consumed_edges.contains(id) {
1890                consumed_edges.push(*id);
1891            }
1892        }
1893    }
1894
1895    // Locate the corner junction on each end face BEFORE trimming (the two
1896    // boundary coedges that meet at the old corner vertex, stepping over the
1897    // pole between them where the end face's apex IS that vertex) — edge ids
1898    // alone are ambiguous on two-coedge cap loops, so the meeting must be at
1899    // the corner.
1900    // (end, transverse_id, corner, first_consumed, second_consumed) -> plan.
1901    // Only FREE ends appear here: a corner stop rebuilds nothing on a third
1902    // face, because the corner patch is what closes it.
1903    let mut end_plans = Vec::with_capacity(2);
1904    for (end, transverse_id, corner, first_consumed, second_consumed) in [
1905        (
1906            start_end.free(),
1907            transverse_a_id,
1908            old_start,
1909            start_first.is_consumed(),
1910            start_second.is_consumed(),
1911        ),
1912        (
1913            finish_end.free(),
1914            transverse_b_id,
1915            old_finish,
1916            finish_first.is_consumed(),
1917            finish_second.is_consumed(),
1918        ),
1919    ] {
1920        let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
1921            continue;
1922        };
1923        let face = result
1924            .shells
1925            .iter()
1926            .flat_map(|shell| &shell.faces)
1927            .find(|face| face.id == end.end_face_id)
1928            .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
1929        // Key on stable coedge ids (not indices) so processing one end can't
1930        // invalidate another that shares this face.
1931        let Some(located) = locate_end_corner(
1932            &result,
1933            face,
1934            corner,
1935            end.first_edge_id,
1936            end.second_edge_id,
1937        ) else {
1938            return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
1939        };
1940        // The apex the transverse curve cuts off goes with the boundaries the
1941        // blend consumes.
1942        for pole in &located.pole_edge_ids {
1943            if !consumed_edges.contains(pole) {
1944                consumed_edges.push(*pole);
1945            }
1946        }
1947        end_plans.push((
1948            end.end_face_id,
1949            transverse_id,
1950            end.transverse_end_pcurve.clone(),
1951            located,
1952            first_consumed,
1953            second_consumed,
1954        ));
1955    }
1956
1957    // Trim the boundary edges at their support crossings.  A consumed side is
1958    // deleted instead of trimmed; a CORNER side touches no boundary at all.
1959    for (rim_resolution, boundary, corner, rim) in [
1960        (
1961            &start_first,
1962            start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
1963            old_start,
1964            w1a,
1965        ),
1966        (
1967            &start_second,
1968            start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
1969            old_start,
1970            w2a,
1971        ),
1972        (
1973            &finish_first,
1974            finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
1975            old_finish,
1976            w1b,
1977        ),
1978        (
1979            &finish_second,
1980            finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
1981            old_finish,
1982            w2b,
1983        ),
1984    ] {
1985        if !rim_resolution.trims_boundary() {
1986            continue;
1987        }
1988        let Some((boundary_id, boundary_param)) = boundary else {
1989            continue;
1990        };
1991        trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
1992    }
1993
1994    // Rebuild each end face's corner: drop the consumed boundary coedge(s) and
1995    // splice in the transverse coedge between the (trimmed) boundaries.
1996    for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
1997        end_plans
1998    {
1999        let forward = located.x_is_first;
2000        let pcurve = if forward {
2001            transverse_end_pcurve
2002        } else {
2003            transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2004        };
2005        let (x_consumed, y_consumed) = if located.x_is_first {
2006            (first_consumed, second_consumed)
2007        } else {
2008            (second_consumed, first_consumed)
2009        };
2010        let transverse_coedge = CoedgeRecord {
2011            id: take_id(),
2012            edge_id: transverse_id,
2013            forward,
2014            pcurve,
2015        };
2016        splice_end_corner(
2017            result,
2018            end_face,
2019            &located,
2020            transverse_coedge,
2021            x_consumed,
2022            y_consumed,
2023        )?;
2024    }
2025
2026    // Blend face: cr fwd -> TB -> cs rev -> TA rev in (t, z) space.  Both rows
2027    // share one parameterisation, so a collapsed cr reads its sense across the
2028    // strip the cs rail still spans (a stripe with BOTH rails collapsed has no
2029    // area; the orchestrator drops it with the edges that bound it).
2030    let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2031        (Some((_, cr_pcurve)), _) => {
2032            let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2033            (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2034        }
2035        (None, Some(_)) => {
2036            let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2037            (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2038        }
2039        (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2040    };
2041    let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2042    let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2043    let out1 = if first.face.same_sense {
2044        n1
2045    } else {
2046        n1.scale(-1.0)
2047    };
2048    let same_sense = blend_normal.dot(out1) >= 0.0;
2049    let loop_id = take_id();
2050    // Traversal senses of the blend's support coedges.  A fresh support edge
2051    // is parameterized in station order, so the sense is the loop's walking
2052    // direction (`blend_cr_forward` for cr, its opposite for cs); a SEWN
2053    // side's sense comes from the detection (the existing edge's own
2054    // parameter direction relative to that same walk — equal to the dropped
2055    // face's old sense, preserving manifold pairing with the survivor).  The
2056    // pcurves always follow the LOOP's walking direction and are unchanged.
2057    let cr_forward = sew_first
2058        .as_ref()
2059        .map_or(blend_cr_forward, |sew| sew.forward);
2060    let cs_forward = sew_second
2061        .as_ref()
2062        .map_or(!blend_cr_forward, |sew| sew.forward);
2063    // The two end slots of the blend loop.  `natural_forward` is the sense the
2064    // loop walks a transverse edge stored first-rim -> second-rim; a CORNER
2065    // arc was already committed in the walk direction (so the corner patch can
2066    // take it the other way round), so it is always used forward.
2067    // A slot is one coedge for a free end (the transverse curve) or a corner
2068    // end (the section arc), and one or two for a miter end (the seam, then
2069    // the sibling's section when the seam left the sibling first).  A corner
2070    // arc was committed in the walk direction; a miter edge records its own
2071    // sense along the walk.
2072    let mut end_slot = |plan: &EndPlan,
2073                        free_edge_id: Option<u64>,
2074                        natural_forward: bool|
2075     -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2076        match plan {
2077            EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2078                id: take_id(),
2079                edge_id: end.arc_edge_id,
2080                forward: true,
2081                pcurve: end.arc_blend_pcurve.clone(),
2082            }]),
2083            EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2084                id: take_id(),
2085                edge_id: end.arc_edge_id,
2086                forward: true,
2087                pcurve: end.arc_blend_pcurve.clone(),
2088            }]),
2089            EndPlan::Miter(end) => end
2090                .edges
2091                .iter()
2092                .map(|(edge_id, forward, pcurve)| {
2093                    Ok(CoedgeRecord {
2094                        id: take_id(),
2095                        edge_id: *edge_id,
2096                        forward: *forward,
2097                        pcurve: pcurve.clone(),
2098                    })
2099                })
2100                .collect(),
2101            EndPlan::Free(free) => {
2102                let edge_id =
2103                    free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2104                Ok(vec![if natural_forward {
2105                    CoedgeRecord {
2106                        id: take_id(),
2107                        edge_id,
2108                        forward: true,
2109                        pcurve: free.transverse_blend_pcurve.clone(),
2110                    }
2111                } else {
2112                    CoedgeRecord {
2113                        id: take_id(),
2114                        edge_id,
2115                        forward: false,
2116                        pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2117                    }
2118                }])
2119            }
2120        }
2121    };
2122    let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2123    let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2124    // A collapsed rail has no coedge: the end slots on either side of it meet
2125    // at its one rim point.
2126    let coedges = if blend_cr_forward {
2127        let mut coedges = Vec::new();
2128        if let Some(cr_edge_id) = cr_edge_id {
2129            coedges.push(CoedgeRecord {
2130                id: take_id(),
2131                edge_id: cr_edge_id,
2132                forward: cr_forward,
2133                pcurve: crate::sweep_topology::parameter_line(
2134                    cr_domain[0],
2135                    0.0,
2136                    cr_domain[1],
2137                    0.0,
2138                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2139            });
2140        }
2141        coedges.extend(finish_slot);
2142        if let Some(cs_edge_id) = cs_edge_id {
2143            coedges.push(CoedgeRecord {
2144                id: take_id(),
2145                edge_id: cs_edge_id,
2146                forward: cs_forward,
2147                pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2148            });
2149        }
2150        coedges.extend(start_slot);
2151        coedges
2152    } else {
2153        let mut coedges = Vec::new();
2154        if let Some(cr_edge_id) = cr_edge_id {
2155            coedges.push(CoedgeRecord {
2156                id: take_id(),
2157                edge_id: cr_edge_id,
2158                forward: cr_forward,
2159                pcurve: crate::sweep_topology::parameter_line(
2160                    cr_domain[1],
2161                    0.0,
2162                    cr_domain[0],
2163                    0.0,
2164                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2165            });
2166        }
2167        coedges.extend(start_slot);
2168        if let Some(cs_edge_id) = cs_edge_id {
2169            coedges.push(CoedgeRecord {
2170                id: take_id(),
2171                edge_id: cs_edge_id,
2172                forward: cs_forward,
2173                pcurve: crate::sweep_topology::parameter_line(
2174                    cs_domain[0],
2175                    1.0,
2176                    cs_domain[1],
2177                    1.0,
2178                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2179            });
2180        }
2181        coedges.extend(finish_slot);
2182        coedges
2183    };
2184    let blend_face_id = take_id();
2185    let mut blend_face = FaceRecord {
2186        id: blend_face_id,
2187        surface: rows.surface,
2188        same_sense,
2189        loops: vec![LoopRecord {
2190            id: loop_id,
2191            coedges,
2192        }],
2193        name: name.map(|value| value.to_string()),
2194    };
2195    if rows.exact_extrusion {
2196        transpose_face(&mut blend_face)?;
2197    }
2198    let shell_index = result
2199        .shells
2200        .iter()
2201        .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2202        .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2203    // Drop the fully-consumed mate faces (lesson 3): their loops collapsed
2204    // between the sewn edge and the blend; the consumed boundaries and
2205    // lesson-7 leftovers are deleted below, the sewn edge lives on shared by
2206    // the blend face and the surviving neighbour.
2207    for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2208        if sew.is_some() {
2209            for shell in &mut result.shells {
2210                shell.faces.retain(|face| face.id != mate.face.id);
2211            }
2212        }
2213    }
2214    result.shells[shell_index].faces.push(blend_face);
2215
2216    result.edges.retain(|candidate| candidate.id != edge.id);
2217    // Delete the boundary edges a prior fillet's corner had left behind that
2218    // this blend fully consumed.
2219    result
2220        .edges
2221        .retain(|candidate| !consumed_edges.contains(&candidate.id));
2222    // Each collapsed rail's two rims are one vertex.
2223    let mut identified = Vec::new();
2224    for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2225        if collapsed && from != to {
2226            identified.push((from, to));
2227        }
2228    }
2229    Ok(SewnStripe {
2230        cr_edge_id,
2231        cs_edge_id,
2232        blend_face_id,
2233        consumed: [sew_first.is_some(), sew_second.is_some()],
2234        snap,
2235        identified,
2236    })
2237}
2238
2239/// Swap the two parameters of a face: the surface's control net, every
2240/// coedge pcurve, and the sense (S_u × S_v changes sign).  The face's
2241/// geometry is untouched; only its chart is.  Used to hand an exact
2242/// cylinder patch built with its section along v to the analytic recogniser,
2243/// which wants the circle along u.
2244pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2245    let surface = &face.surface;
2246    let rows_u = surface.control_points.len();
2247    let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2248    let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2249    for row in &surface.control_points {
2250        for (j, point) in row.iter().enumerate() {
2251            transposed[j].push(*point);
2252        }
2253    }
2254    face.surface = NurbsSurface::new(
2255        surface.degree_v,
2256        surface.degree_u,
2257        surface.knots_v.clone(),
2258        surface.knots_u.clone(),
2259        transposed,
2260    ).or_refuse(KernelStage::Refine, "new")?;
2261    for loop_record in &mut face.loops {
2262        for coedge in &mut loop_record.coedges {
2263            for control in &mut coedge.pcurve.control_points {
2264                std::mem::swap(&mut control.x, &mut control.y);
2265            }
2266        }
2267    }
2268    face.same_sense = !face.same_sense;
2269    Ok(())
2270}
2271
2272/// Drop every vertex no longer referenced by an edge — the sharp corners the
2273/// blends replaced, and anything freed by a consumed boundary.  Run ONCE after
2274/// a whole group of stripes is in: a corner vertex is still referenced by the
2275/// selected edges that have not been sewn yet.
2276pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2277    let used: rustc_hash::FxHashSet<u64> = result
2278        .edges
2279        .iter()
2280        .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2281        .collect();
2282    result
2283        .vertices
2284        .retain(|candidate| used.contains(&candidate.id));
2285}
2286
2287fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2288    crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2289}
2290