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    read_rail_windows([&rows.cr, &rows.cs], parameters, stations, carriers, windows,
701        rows.exact_extrusion && affine_reads_on(), None)
702}
703
704fn read_rail_windows(
705    rails: [&NurbsCurve; 2], parameters: &[f64], stations: &[Station],
706    carriers: [&NurbsSurface; 2], windows: [Option<[f64; 2]>; 2], affine: bool,
707    mut memo: Option<&mut RailFeet>,
708) -> (f64, [usize; 2]) {
709    let mut worst = 0.0_f64;
710    let mut samples = [0usize; 2];
711    for (side, (rail, carrier)) in [(rails[0], carriers[0]), (rails[1], carriers[1])].into_iter().enumerate() {
712        let Some([low, high]) = windows[side] else { continue };
713        // An exact extrusion's straight rail on a plane: the ends carry the
714        // worst (`affine_rail_standoff`), the samples between them cannot.
715        if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
716            crate::blend::record_count("count.read.affine_rails", 1);
717            samples[side] += 2;
718            if !worst.is_nan() && off > worst {
719                worst = off;
720            }
721            continue;
722        }
723        let mut at = vec![low, high];
724        for pair in parameters.windows(2) {
725            for fraction in [0.25, 0.5, 0.75] {
726                let u = pair[0] + (pair[1] - pair[0]) * fraction;
727                if u > low && u < high {
728                    at.push(u);
729                }
730            }
731        }
732        for u in at {
733            let cached = memo.as_ref().and_then(|memo| memo.values[side].get(&u.to_bits()).copied());
734            let off = if let Some(off) = cached {
735                if let Some(memo) = memo.as_mut() { memo.hits += 1; }
736                crate::blend::record_count("count.read.rail_foot_reused", 1);
737                off
738            } else {
739            if let Some(memo) = memo.as_mut() {
740                memo.reads += 1;
741                crate::blend::record_count("count.read.rail_foot_fresh", 1);
742            }
743            let off = interval_at(parameters, u)
744                .and_then(|(interval, fraction)| {
745                    let (left, right) = (&stations[interval], &stations[interval + 1]);
746                    let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
747                    let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
748                    let point = rail.evaluate(u).ok()?;
749                    crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
750                })
751                .filter(|off| off.is_finite())
752                .unwrap_or(f64::NAN);
753            if off.is_finite() {
754                if let Some(memo) = memo.as_mut() { memo.values[side].insert(u.to_bits(), off); }
755            }
756            off
757            };
758            samples[side] += 1;
759            if !worst.is_nan() && (off.is_nan() || off > worst) {
760                worst = off;
761            }
762        }
763    }
764    (worst, samples)
765}
766
767/// A construction-local rail reader. It owns an immutable snapshot of every
768/// input to a foot reading; only the requested windows can change. Native
769/// parameter bits identify a sample, so nearby endpoints never share a foot.
770/// The network keeps this reader from its chosen rung through shipped-window
771/// acceptance, then drops it. Failed/nonfinite feet are never reused.
772pub(in crate::blend) struct RailWindowReader {
773    rails: [NurbsCurve; 2],
774    carriers: [NurbsSurface; 2],
775    parameters: Vec<f64>,
776    stations: Vec<Station>,
777    affine: bool,
778    feet: RailFeet,
779}
780
781#[derive(Default)]
782struct RailFeet {
783    values: [rustc_hash::FxHashMap<u64, f64>; 2],
784    reads: u64,
785    hits: u64,
786}
787
788impl RailWindowReader {
789    pub(in crate::blend) fn new(rows: &FittedRows, parameters: &[f64], stations: &[Station], carriers: [&NurbsSurface; 2]) -> Self {
790        Self {
791            rails: [rows.cr.clone(), rows.cs.clone()],
792            carriers: [carriers[0].clone(), carriers[1].clone()],
793            parameters: parameters.to_vec(), stations: stations.to_vec(),
794            affine: rows.exact_extrusion && affine_reads_on(), feet: RailFeet::default(),
795        }
796    }
797
798    pub(in crate::blend) fn read(&mut self, windows: [Option<[f64; 2]>; 2]) -> (f64, [usize; 2]) {
799        read_rail_windows([&self.rails[0], &self.rails[1]], &self.parameters, &self.stations,
800            [&self.carriers[0], &self.carriers[1]], windows, self.affine, Some(&mut self.feet))
801    }
802}
803
804pub(in crate::blend) fn network_rail_reuse_on() -> bool {
805    // The private halving diagnostic compares every requested foot against
806    // its reference algorithm, including repeats. Preserve that coverage.
807    std::env::var("BREP_NETWORK_RAIL_REUSE").map_or(true, |value| value != "0" && value != "off")
808}
809
810/// The JUNCTION stencil across the station between interval `left` and
811/// `left + 1`: `left_nodes` (33 exact centres of `left`) from node 16 to 32,
812/// then `right_nodes` (33 of `left + 1`) from node 1 to 16, the shared
813/// station at node 16. Its native abscissae come from `station_ts` (the
814/// edge parameters the centres were solved at): `None` when the two native
815/// widths are bitwise equal -- uniform nodes, read by the ordinary reader --
816/// else each node's t, interior nodes by the dense solves' own arithmetic
817/// t_i + (t_{i+1} − t_i)·j/32 and the shared station at its STORED t. Err
818/// when a station t is not finite or a width is not strictly positive.
819pub(in crate::blend) fn junction_stencil(
820    left_nodes: &[Vec3],
821    right_nodes: &[Vec3],
822    station_ts: &[f64],
823    left: usize,
824) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
825    let half = OPEN_WALL_CENTRES / 2;
826    if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
827        return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
828    }
829    let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
830    let (w_left, w_right) = (t1 - t0, t2 - t1);
831    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()) {
832        return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
833    }
834    let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
835    if w_left.to_bits() == w_right.to_bits() {
836        return Ok((nodes, None));
837    }
838    let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
839        .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
840        .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
841        .collect();
842    Ok((nodes, Some(abscissae)))
843}
844
845/// One wall probe on a stencil: the ordinary uniform reader when it has no
846/// native abscissae (an interval's own nodes, or a junction of equal native
847/// widths), the native-abscissa reader when it has them.
848pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
849    match abscissae {
850        None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
851        Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
852    }
853}
854
855/// Every interval's largest DECLARED wall reading (`chain::declared_wall_probe`,
856/// reading + uncertainty) on the open fillet `rows`, against the exact ball
857/// centres the march solved at j/32 of each interval ([`OpenDenseCentres`]),
858/// or, for each unread interval, WHY: its station
859/// interval and which component failed (no dense row, a dense row of the
860/// wrong size, an exact centre the march did not solve, a wall point the
861/// surface would not evaluate, a probe the declared reader declined, no probe
862/// in the shipped part, or a non-finite reading). The cause is diagnostic.
863///
864/// Read on the DECLARED SHIPPED REGION `window` only: an interval it overlaps
865/// is probed at its even stencil fractions that lie inside the window and at
866/// each window end inside the interval; a probe outside the window is not on
867/// the shipped wall and is not read. A window END whose nearest even stencil
868/// node falls outside 2..=30 -- an end within a node of the interval's own
869/// station, as the original bore's shipped spans were, 1e-16 from their
870/// vertex stations -- has its foot at the interval's end node, where
871/// [`crate::blend::chain::declared_wall_probe`] cannot read (the stencil
872/// cannot follow a foot out of its own interval). It is read on a JUNCTION
873/// stencil instead: the neighbouring interval's last half and this one's
874/// first half (or this one's last half and the next one's first), the same
875/// exact centres meeting at the shared station, with the probe at its
876/// middle node. Two intervals of unequal native width (local insertions)
877/// give the junction unequal node spacing, so it is then read by the
878/// native-abscissa reader (`chain::declared_wall_probe_at_why`) on the
879/// edge parameters the centres were solved at; of equal width, by the
880/// ordinary reader. With no neighbour it is read as before.
881/// The wall reading of an EXACT extrusion's rows, read directly: the wall is
882/// the ball's sweep along the centre line, so every point of the shipped
883/// surface stands exactly the radius off that line, and the reading is how
884/// far it does not. One entry per interval overlapping `window`, probes at
885/// the interval's quarters inside the window and at the window's ends, over
886/// the section at k/8 (the ball-resolve reader's sections). Never for fitted
887/// rows: those are read against the exact centres between their stations
888/// ([`open_wall_readings_why`]).
889pub(in crate::blend) fn exact_extrusion_wall_readings(
890    rows: &FittedRows,
891    parameters: &[f64],
892    window: [f64; 2],
893    radius: f64,
894) -> Vec<Result<f64, String>> {
895    let intervals = parameters.len().saturating_sub(1);
896    let overlapped: Vec<usize> =
897        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
898    let line = match (rows.exact_extrusion, rows.center.as_ref()) {
899        (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
900        (false, _) => Err("the rows are not an exact extrusion".to_string()),
901        (true, None) => Err("the exact rows carry no centre line".to_string()),
902    };
903    let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
904        Ok(line) => line,
905        Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
906    };
907    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
908        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
909    };
910    overlapped
911        .into_iter()
912        .map(|interval| -> Result<f64, String> {
913            let (from, to) = (parameters[interval], parameters[interval + 1]);
914            let inside = |u: f64| u >= window[0] && u <= window[1];
915            let mut probes: Vec<f64> =
916                (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
917            probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
918            if probes.is_empty() {
919                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
920            }
921            let mut worst = 0.0_f64;
922            for u in probes {
923                for k in 1..OPEN_WALL_SECTIONS {
924                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
925                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
926                    let offset = point.sub(anchor);
927                    let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
928                    let reading = (standoff - radius).abs();
929                    if !reading.is_finite() {
930                        return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
931                    }
932                    worst = worst.max(reading);
933                }
934            }
935            Ok(worst)
936        })
937        .collect()
938}
939
940/// The wall reading of an EXACT REVOLUTION's rows
941/// (`support::exact_revolution_rows`), read directly: the wall is the ball's
942/// sweep along the centre CIRCLE, so every point of the shipped surface
943/// stands exactly the radius off that circle, and the reading is how far it
944/// does not. The distance to the circle is closed-form in the point's
945/// meridian half-plane: `hypot(ρ − R_c, h − h_c)` for the point's radial
946/// distance ρ and axial height h about the rows' axis. Same probes as
947/// [`exact_extrusion_wall_readings`]: one entry per interval overlapping
948/// `window`, the interval's quarters inside the window and the window's
949/// ends, over the section at k/8.
950pub(in crate::blend) fn exact_revolution_wall_readings(
951    rows: &FittedRows,
952    parameters: &[f64],
953    window: [f64; 2],
954    radius: f64,
955) -> Vec<Result<f64, String>> {
956    let intervals = parameters.len().saturating_sub(1);
957    let overlapped: Vec<usize> =
958        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
959    let circle = match (rows.revolution_axis, rows.center.as_ref()) {
960        (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
961        (None, _) => Err("the rows are not an exact revolution".to_string()),
962        (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
963    };
964    let (origin, axis, centre_radius, centre_height) = match circle {
965        Ok((origin, axis, point)) => {
966            let offset = point.sub(origin);
967            let height = offset.dot(axis);
968            (origin, axis, offset.sub(axis.scale(height)).length(), height)
969        }
970        Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
971    };
972    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
973        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
974    };
975    overlapped
976        .into_iter()
977        .map(|interval| -> Result<f64, String> {
978            let (from, to) = (parameters[interval], parameters[interval + 1]);
979            let inside = |u: f64| u >= window[0] && u <= window[1];
980            let mut probes: Vec<f64> =
981                (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
982            probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
983            if probes.is_empty() {
984                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
985            }
986            let mut worst = 0.0_f64;
987            for u in probes {
988                for k in 1..OPEN_WALL_SECTIONS {
989                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
990                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
991                    let offset = point.sub(origin);
992                    let height = offset.dot(axis);
993                    let radial = offset.sub(axis.scale(height)).length();
994                    let standoff = (radial - centre_radius).hypot(height - centre_height);
995                    let reading = (standoff - radius).abs();
996                    if !reading.is_finite() {
997                        return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
998                    }
999                    worst = worst.max(reading);
1000                }
1001            }
1002            Ok(worst)
1003        })
1004        .collect()
1005}
1006
1007pub(in crate::blend) fn open_wall_readings_why(
1008    rows: &FittedRows,
1009    parameters: &[f64],
1010    stations: &[Station],
1011    window: [f64; 2],
1012    dense: &OpenDenseCentres,
1013    radius: f64,
1014) -> Vec<Result<f64, String>> {
1015    let intervals = parameters.len().saturating_sub(1);
1016    let overlapped: Vec<usize> =
1017        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1018    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1019        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1020    };
1021    // The exact centres of one interval at j/32, its stations at both ends.
1022    let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
1023        let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
1024        if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
1025            return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
1026        }
1027        let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
1028        centres.push(stations[interval].center);
1029        for (j, centre) in inner.iter().enumerate() {
1030            centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
1031        }
1032        centres.push(stations[interval + 1].center);
1033        Ok(centres)
1034    };
1035    let half = OPEN_WALL_CENTRES / 2;
1036    // The junction stencil across the station between `left` and `left + 1`:
1037    // left's nodes half..=32, then right's nodes 1..=half; the station is node
1038    // `half`. With each node's NATIVE abscissa (the edge parameter its exact
1039    // centre was solved at, `OpenDenseCentres::station_ts`), and `None` when
1040    // the two intervals' native widths are bitwise equal: the nodes are then
1041    // uniform and the ordinary reader reads them, bit for bit.
1042    let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
1043        if dense.station_ts.len() != parameters.len() {
1044            return Err(format!("interval {left}: junction without the native station parameters"));
1045        }
1046        junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
1047    };
1048    overlapped
1049        .into_iter()
1050        .map(|interval| -> Result<f64, String> {
1051            let centres = nodes_of(interval)?;
1052            let (from, to) = (parameters[interval], parameters[interval + 1]);
1053            let inside = |u: f64| u >= window[0] && u <= window[1];
1054            // (u, stencil node, the stencil: None = this interval's own;
1055            // a junction's nodes with its native abscissae when unequal).
1056            let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
1057                .step_by(2)
1058                .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
1059                .filter(|(u, _)| inside(*u))
1060                .map(|(u, step)| (u, step, None))
1061                .collect();
1062            for end in window {
1063                if end > from && end < to {
1064                    let fraction = (end - from) / (to - from);
1065                    let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
1066                    #[cfg(not(test))]
1067                    let junction_off = false;
1068                    if junction_off {
1069                        probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1070                    } else if nearest < 2 && interval > 0 {
1071                        probes.push((end, half, Some(junction(interval - 1)?)));
1072                    } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
1073                        probes.push((end, half, Some(junction(interval)?)));
1074                    } else {
1075                        probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1076                    }
1077                }
1078            }
1079            if probes.is_empty() {
1080                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1081            }
1082            let mut declared = 0.0_f64;
1083            for (u, step, stencil) in probes {
1084                let (nodes, abscissae, what) = match &stencil {
1085                    Some((nodes, None)) => (nodes, None, "junction"),
1086                    Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
1087                    None => (&centres, None, "interval"),
1088                };
1089                for k in 1..OPEN_WALL_SECTIONS {
1090                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1091                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1092                    // The interval's own stencil reads from the probe's node,
1093                    // then (declined) from where its fraction reaches along the
1094                    // centre path (a stationary section); a junction keeps its
1095                    // own dispatch.
1096                    let read = if stencil.is_none() {
1097                        crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
1098                    } else {
1099                        read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
1100                    };
1101                    let (reading, uncertainty) = read.map_err(|why| {
1102                        format!(
1103                            "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
1104                             (u {u:.17}): {why}"
1105                        )
1106                    })?;
1107                    declared = declared.max(reading + uncertainty);
1108                }
1109            }
1110            if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
1111        })
1112        .collect()
1113}
1114
1115
1116/// A fresh id allocator seeded past the vertex, edge, face, loop and coedge IDs.
1117/// Solid and shell IDs are outside this allocation domain. Shared by
1118/// a whole group of stripes so their vertices, edges and coedges cannot
1119/// collide.
1120pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
1121    let mut next_id = solid
1122        .vertices
1123        .iter()
1124        .map(|vertex| vertex.id)
1125        .chain(solid.edges.iter().map(|edge| edge.id))
1126        .chain(
1127            solid
1128                .shells
1129                .iter()
1130                .flat_map(|shell| &shell.faces)
1131                .flat_map(|face| {
1132                    face.loops
1133                        .iter()
1134                        .map(|loop_record| loop_record.id)
1135                        .chain(face.loops.iter().flat_map(|loop_record| {
1136                            loop_record.coedges.iter().map(|coedge| coedge.id)
1137                        }))
1138                        .chain(std::iter::once(face.id))
1139                }),
1140        )
1141        .max()
1142        .unwrap_or(0)
1143        + 1;
1144    move || {
1145        let id = next_id;
1146        next_id += 1;
1147        id
1148    }
1149}
1150
1151
1152/// May a declined support refit fall back on the march's own trim pcurve?
1153/// Only when that trim, read independently as shipped
1154/// (`read_shipped_pcurve`: miss, rail standoff, samples, worst), meets the
1155/// floor ON its rail's branch: its miss PLUS the rail's standoff from the
1156/// carrier at the trim's foot within `floor`. A trim on another branch of the
1157/// carrier tracks its own foot perfectly (miss 0) while that foot stands far
1158/// off the rail, so a miss alone would ship it; an unreadable trim never
1159/// stands.
1160pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
1161    matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
1162}
1163
1164/// Slice 1b of the surgery lane (`exact_trim_on`): the support trim of a
1165/// straight rail on a PLANE, exact. The feet of the rail's two ends on the
1166/// plane (the kernel's closed-form projection) joined by a degree-1 pcurve
1167/// on the rail's own parameter range, so the surgery reads it at the rail's
1168/// parameter as it reads every trim; read once as shipped
1169/// (`read_shipped_pcurve`) and kept only when it stands within the pcurve
1170/// floor by the march trim's own rule (`march_trim_stands`: miss plus rail
1171/// standoff). `None` hands the trim to the refit road, as a march trim that
1172/// does not stand would be.
1173fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
1174    if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
1175        return Ok(None);
1176    }
1177    let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1178    let mut control = Vec::with_capacity(2);
1179    for t in [d0, d1] {
1180        let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1181        let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1182        control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1183    }
1184    let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1185    feet_timing.stop();
1186    let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1187    let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
1188        Some(read) => {
1189            crate::blend::record_count("count.read.affine_trims", 1);
1190            Ok(read)
1191        }
1192        None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
1193    };
1194    read_timing.stop();
1195    Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1196}
1197
1198/// [`read_shipped_pcurve`](crate::blend::track_fit::read_shipped_pcurve)'s
1199/// `(miss, standoff, samples, worst)` for a degree-1 `pcurve` on an AFFINE
1200/// plane against an affine rail, read at `d0` and `d1` only: the pcurve's
1201/// image, the rail and the rail's foot on the plane are all affine in the
1202/// rail parameter, so the miss (image to foot) and the standoff (rail to foot)
1203/// are norms of affine functions, convex, and largest at an end. `None` when
1204/// any of the three is not affine, or an end will not evaluate.
1205fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
1206    if !affine_curve(rail) || !affine_curve(pcurve) {
1207        return None;
1208    }
1209    let (origin, normal) = affine_plane(surface)?;
1210    let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
1211    for t in [d0, d1] {
1212        let point = rail.evaluate(t).ok()?;
1213        let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
1214        let uv = pcurve.evaluate(t).ok()?;
1215        let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
1216        let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
1217        if !(residual.is_finite() && off.is_finite()) {
1218            return None;
1219        }
1220        if residual > miss {
1221            (miss, worst) = (residual, t);
1222        }
1223        standoff = standoff.max(off);
1224    }
1225    Some((miss, standoff, 2, worst))
1226}
1227
1228/// Sew ONE stripe into `result`.
1229///
1230/// `solid` is the ORIGINAL topology every stripe was marched against (the
1231/// row-coincidence detection reads it); `result` is the shared evolving
1232/// solid, and `take_id` the shared id allocator — a group of stripes meeting
1233/// at a corner must create their rim vertices and cross-section arcs from one
1234/// counter, and must see each other's work.
1235///
1236/// Each end is either free (terminate on the face across the corner) or a
1237/// corner stop; see [`EndPlan`].  Pruning orphaned vertices is the caller's
1238/// job, once, after every stripe of the group is in.
1239pub(in crate::blend) fn build_open_surgery(
1240    solid: &BrepSolid,
1241    result: &mut BrepSolid,
1242    take_id: &mut dyn FnMut() -> u64,
1243    edge: &EdgeRecord,
1244    first: &BlendMate,
1245    second: &BlendMate,
1246    rows: FittedRows,
1247    ends: [EndPlan; 2],
1248    name: Option<&str>,
1249) -> Result<SewnStripe, KernelRefusal> {
1250    let [start_end, finish_end] = ends;
1251
1252    // Trim the support rows to the crossing window.
1253    // A POLE end stops EXACTLY on the row's own end (the march puts the
1254    // pole's station there and overshoots no further), so there is nothing to
1255    // split off on that side.  Only equality: a stop PAST the row's end — a
1256    // free end's crossing the rows never reached — still fails the split and
1257    // refuses, rather than building a rail out to wherever the rows stop.
1258    let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1259        let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1260        let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1261        if b == high {
1262            return Ok(tail);
1263        }
1264        let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1265        Ok(middle)
1266    };
1267    let kind = |plan: &EndPlan| match plan {
1268        EndPlan::Free(_) => "free",
1269        EndPlan::Corner(_) => "corner",
1270        EndPlan::Miter(_) => "miter",
1271        EndPlan::Cap(_) => "cap",
1272    };
1273    let describe_trim = |error: KernelRefusal| {
1274        error.with_message(|error| {
1275            format!(
1276                "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1277                 at cr {:.6}/cs {:.6})",
1278                edge.id,
1279                kind(&start_end),
1280                start_end.cr_parameter(),
1281                start_end.cs_parameter(),
1282                kind(&finish_end),
1283                finish_end.cr_parameter(),
1284                finish_end.cs_parameter()
1285            )
1286        })
1287    };
1288    // A rail whose two stops are one point within `consumed_band` has no strip
1289    // to trim: the blend takes that mate down to a single point along this
1290    // edge.  That is a FULL-WIDTH corner — the shared face of a miter whose
1291    // radius is the side face's width, every face of a star at full width, the
1292    // middle edge of a channel whose two corner setbacks meet.  No rail edge is
1293    // built; the mate loses the blended edge instead of having it replaced, and
1294    // the rail's two rim vertices are one vertex, which the orchestrator
1295    // identifies (`SewnStripe::identified`) once every stripe is in.  A stripe
1296    // free at BOTH ends has no corner to collapse onto, so it refuses.
1297    let band = consumed_band(solid);
1298    let exact_trim = crate::blend::exact_trim_on();
1299    let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1300        let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1301        Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1302            && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1303    };
1304    let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1305    let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1306    if cr_collapsed || cs_collapsed {
1307        let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1308            Some("neither end is a corner")
1309        } else if first.face.id == second.face.id {
1310            Some("both rails lie on one face")
1311        } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1312            Some("a capped end has no corner to share the point with")
1313        } else {
1314            None
1315        };
1316        if let Some(reason) = unsupported {
1317            return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1318                "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1319                edge.id
1320            )));
1321        }
1322    }
1323    // The support pcurve the rows carry interpolates the stations' (u, v) and
1324    // is read nowhere between them: on the 20-degree crossing's notched open
1325    // exit arc it stood 2.98e-5 off the rail it trims (q1 = 9cd8c3163 + the
1326    // section-fit fix, read against the exact cylinders). Inside the crossing
1327    // window — on the face, never on a carrier's extension — it is refitted
1328    // to the trimmed rail's own projected track at the pcurve floor and read
1329    // again as shipped (`refit_closed_support_pcurve`, the closed edge's).
1330    // A rail that overruns a PLANE carrier's chart (the rib-base fin top's
1331    // rails run 0.303 past it) is on the plane's extension, which the chart
1332    // clamps: there the refit and both reads would measure the clamp, and the
1333    // trim is instead rebuilt from its edge on the widened chart and verified
1334    // at the floor by `fit_planar_charts_to_trims`, which every lane that
1335    // sews these stripes runs.
1336    let trim_rail = |collapsed: bool,
1337                     row: &NurbsCurve,
1338                     pcurve: &NurbsCurve,
1339                     surface: &NurbsSurface,
1340                     a: f64,
1341                     b: f64|
1342     -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1343        if collapsed {
1344            return Ok(None);
1345        }
1346        let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1347        let rail = trim_row(row, a, b).map_err(describe_trim)?;
1348        let interpolated = trim_row(pcurve, a, b)?;
1349        rows_timing.stop();
1350        {
1351            let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1352            let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1353            let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1354            overrun_timing.stop();
1355            if overrun.is_some_and(|excursion| excursion > band) {
1356                return Ok(Some((rail, interpolated)));
1357            }
1358        }
1359        let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1360        // Slice 1b (`exact_trim_on`): an exact extrusion row's rail on a
1361        // plane ships the exact line in uv, read once; the refit road below
1362        // is for what it declines.
1363        if exact_trim && rows.exact_extrusion {
1364            if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1365                crate::blend::record_count("count.surgery.exact_trim", 1);
1366                return Ok(Some((rail, line)));
1367            }
1368        }
1369        // The exact REVOLUTION rows' own trims are exact by construction
1370        // (the carrier's uv line, the plane's affine image of the rail):
1371        // read once as shipped by the march trim's rule (`march_trim_stands`)
1372        // and kept; whatever does not stand takes the refit road below.
1373        if rows.revolution_axis.is_some() {
1374            let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1375            let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1376            read_timing.stop();
1377            if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1378                crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1379                return Ok(Some((rail, interpolated)));
1380            }
1381            crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1382        }
1383        // The march's OWN trim pcurve, read independently at the rail's own
1384        // parameter against the same floor (`read_shipped_pcurve`): what a
1385        // declined refit may fall back to, and the standoff a refit on the
1386        // rail's own branch must match.
1387        // The feet of each pcurve against the rail are sought once and
1388        // shared between the reads below (`FootMemo`): the march trim's for
1389        // its own read and the branch comparison, the refit's for the
1390        // refit's own read, the branch comparison and the absolute bound.
1391        let mut march_feet = crate::blend::track_fit::FootMemo::default();
1392        let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1393        let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1394        let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1395        march_read_timing.stop();
1396        let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1397        let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1398            // Kept ONLY when the march's trim stands on its own read
1399            // (`march_trim_stands`); otherwise the refusal stands.
1400            let stands = march_trim_stands(&march_read, floor);
1401            if stands {
1402                Ok(Some((rail.clone(), interpolated.clone())))
1403            } else {
1404                Err(why)
1405            }
1406        };
1407        let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1408        let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1409        refit_timing.stop();
1410        let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1411        match refit {
1412            Ok((fitted, refit_read)) => {
1413                // A refit tracks ITS OWN projected track; a track that took
1414                // another branch of the carrier tracks itself perfectly while
1415                // its foot stands far off the rail (the stationary-start
1416                // prism's coedge read 6.0 off its edge). Its foot standoff
1417                // must be the rail's own standoff on the march trim's branch.
1418                // BOTH reads are required: an unreadable refit read declines
1419                // the refit (the march trim then stands only on its own read
1420                // within the floor), and an unreadable march read cannot
1421                // establish that the refit is on the rail's branch, so it
1422                // refuses. Never shipped on an unknown.
1423                // The two are compared POINTWISE, at the same rail
1424                // parameters (`standoff_excess`), never as two maxima read at
1425                // each curve's own knots.
1426                // The refit's read is the shipped curve's own, handed back by
1427                // the refit (the same `read_shipped_pcurve` on the same
1428                // inputs, once).
1429                let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1430                    (_, Err(error)) => return declined(error),
1431                    (Err(error), Ok(_)) => {
1432                        return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1433                            "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1434                             trim is unreadable ({})",
1435                            error.message
1436                        )));
1437                    }
1438                    (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1439                };
1440                let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1441                let excess = crate::blend::track_fit::standoff_excess_memo(
1442                    surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1443                );
1444                excess_timing.stop();
1445                let excess = match excess {
1446                    Ok(excess) => excess,
1447                    Err(error) => return declined(error),
1448                };
1449                if !(excess <= floor) {
1450                    return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1451                        "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1452                         at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1453                         another branch of the carrier"
1454                    )));
1455                }
1456                // ABSOLUTE: the comparison above is relative to the march
1457                // trim, so a refit that follows an off-branch march trim
1458                // passes it. The refit's foot must also stand within the floor
1459                // of the rail's own deviation from its carrier, read by the
1460                // nearest-point projector at the same parameters
1461                // (`standoff_over_nearest`) -- the rail's approximation the
1462                // lane reports, never more.
1463                let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1464                let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1465                nearest_timing.stop();
1466                let absolute = match absolute {
1467                    Ok(absolute) => absolute,
1468                    Err(error) => return declined(error),
1469                };
1470                if !(absolute <= floor) {
1471                    return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1472                        "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1473                         off its carrier: its foot is not the rail's nearest point"
1474                    )));
1475                }
1476                Ok(Some((rail, fitted)))
1477            }
1478            Err(error) => declined(error),
1479        }
1480    };
1481    let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1482    let cr_rail = trim_rail(
1483        cr_collapsed,
1484        &rows.cr,
1485        &rows.cr_pcurve,
1486        &first.face.surface,
1487        start_end.cr_parameter(),
1488        finish_end.cr_parameter(),
1489    )?;
1490    let cs_rail = trim_rail(
1491        cs_collapsed,
1492        &rows.cs,
1493        &rows.cs_pcurve,
1494        &second.face.surface,
1495        start_end.cs_parameter(),
1496        finish_end.cs_parameter(),
1497    )?;
1498    trim_timing.stop();
1499    // A collapsed rail's domain is its two stops, which are one point.
1500    let cr_domain = match &cr_rail {
1501        Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1502        None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1503    };
1504    let cs_domain = match &cs_rail {
1505        Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1506        None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1507    };
1508    // A rail's end points, read off the trimmed rail or, collapsed, the row.
1509    let cr_at = |parameter: f64| match &cr_rail {
1510        Some((cr, _)) => cr.evaluate(parameter),
1511        None => rows.cr.evaluate(parameter),
1512    };
1513    let cs_at = |parameter: f64| match &cs_rail {
1514        Some((cs, _)) => cs.evaluate(parameter),
1515        None => rows.cs.evaluate(parameter),
1516    };
1517    let old_start = edge.start_vertex_id;
1518    let old_finish = edge.end_vertex_id;
1519
1520    // Classify each of the four support crossings.  A crossing that lands at
1521    // the FAR endpoint of the boundary edge it meets (the end away from the
1522    // corner) means a prior fillet already consumed that whole boundary: the
1523    // new blend reuses the existing vertex there and the boundary edge is
1524    // deleted (its role is taken over by the new transverse curve on the
1525    // prior blend face).  An interior crossing is the pristine case — a fresh
1526    // vertex with the boundary trimmed to it.
1527    //
1528    // Consumed means within `consumed_band` — the slack `check_support_extent`
1529    // refuses past, so a rail between the two is not possible.
1530    let classify =
1531        |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1532            let boundary = result
1533                .edges
1534                .iter()
1535                .find(|candidate| candidate.id == boundary_id)
1536                .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1537            let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1538            let far_vertex = if boundary.start_vertex_id == corner {
1539                Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1540            } else if boundary.end_vertex_id == corner {
1541                Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1542            } else {
1543                None
1544            };
1545            if let Some((far_id, far_t, near_t)) = far_vertex {
1546                // A consumed boundary is a COINCIDENCE — a prior fillet's rim
1547                // vertex is exactly where this rail crosses — and the operands
1548                // were healed before the march, so it holds to solver
1549                // precision.  It is not "near the far end": a fillet whose
1550                // radius is 1e-3 short of the face's width crosses 1e-3 from
1551                // the far vertex and must keep that sliver, or the face is
1552                // snapped a full 1e-3 out of true (the oversized-radius limit
1553                // cases and issue 1177 measure exactly that).  Measured in
1554                // model units, never in the boundary's parameter — a unit
1555                // parameter on a 20-long edge would call 2e-3 a coincidence.
1556                let far_point = result
1557                    .vertices
1558                    .iter()
1559                    .find(|candidate| candidate.id == far_id)
1560                    .map(|candidate| candidate.point)
1561                    .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1562                let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1563                let _ = span;
1564                let past_far = crossing.sub(far_point).length();
1565                if past_far <= band {
1566                    // Sanity: the reused vertex must exist.
1567                    if !result
1568                        .vertices
1569                        .iter()
1570                        .any(|candidate| candidate.id == far_id)
1571                    {
1572                        return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1573                    }
1574                    return Ok(RimResolution::Consumed(far_id));
1575                }
1576                // A crossing BEYOND the far vertex is a rail that runs off
1577                // its face: the blend is wider than the face by more than the
1578                // band a snap may close, and trimming the boundary there would
1579                // extend it into air.  `check_support_extent` refuses this
1580                // before the march on every face the exact tool understands;
1581                // this is the refusal for the rest.  It is not the whole
1582                // answer: a crossing the solve CLAMPS to the far vertex reads
1583                // as consumed here, and `detect_row_coincidence` is what sees
1584                // that row leave the face.  Measured on the census: it fires on
1585                // a wall wider than its closed chain's arc (0.5 past) and on the
1586                // r = 10.001 pinch past an earlier round (2e-3 past), both of
1587                // which refused later in the surgery before.
1588                if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1589                    return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1590                        "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1591                         past that edge's far vertex, and a rail within {band:.3e} of it is the \
1592                         most this surgery snaps onto the edge"
1593                    )));
1594                }
1595            }
1596            Ok(RimResolution::Fresh)
1597        };
1598    // A CORNER end has no boundary to classify: the ball's tangency vertex is
1599    // the rim, and the boundary edge that used to reach the sharp corner is
1600    // the neighbouring stripe's own blended edge, which that stripe replaces.
1601    let resolve_side = |plan: &EndPlan,
1602                        side_first: bool,
1603                        corner: u64|
1604     -> Result<RimResolution, KernelRefusal> {
1605        if let Some(vertex) = plan.planned_rim(side_first) {
1606            return Ok(RimResolution::Corner(vertex));
1607        }
1608        match plan {
1609            EndPlan::Free(free) => classify(
1610                if side_first {
1611                    free.first_edge_id
1612                } else {
1613                    free.second_edge_id
1614                },
1615                if side_first {
1616                    free.first_edge_parameter
1617                } else {
1618                    free.second_edge_parameter
1619                },
1620                corner,
1621            ),
1622            EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1623                Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1624            }
1625        }
1626    };
1627    let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1628    let start_first = resolve_side(&start_end, true, old_start)?;
1629    let start_second = resolve_side(&start_end, false, old_start)?;
1630    let finish_first = resolve_side(&finish_end, true, old_finish)?;
1631    let finish_second = resolve_side(&finish_end, false, old_finish)?;
1632    crossings_timing.stop();
1633    let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1634
1635    // Manifold pairing: the blend's use of the first support row must oppose
1636    // F1's use of the blended edge (see the blend-loop construction below).
1637    let first_use_forward = first.coedge.forward;
1638    let blend_cr_forward = !first_use_forward;
1639
1640    // Curve-level coincidence (OCCT PR #1449 lesson 3): when BOTH of a mate's
1641    // crossings are endpoint-consumed AND the trimmed support row retraces the
1642    // existing edge joining the two far vertices, that mate face is FULLY
1643    // consumed — the surgery sews the blend face straight onto the existing
1644    // edge and drops the zero-width face, instead of creating a coincident
1645    // fresh support edge that would leave a sliver strip (the r = face-width
1646    // class).  Each side is detected independently; any ambiguity keeps the
1647    // pristine path (fail-safe).  `row_traversed_from_start`: the blend loop
1648    // walks cr in station order iff `blend_cr_forward`, and cs in the
1649    // OPPOSITE order (see the two loop branches below).
1650    // A stripe with a CORNER end has no boundary edges at that end to be
1651    // coincident with, so the detection only runs on stripes that are free at
1652    // both ends.  (Fail-safe: not detecting a coincidence keeps the pristine
1653    // path, which is what a corner stop wants anyway.)
1654    let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1655        (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1656            detect_row_coincidence(
1657                solid,
1658                first,
1659                edge.id,
1660                start_free.first_edge_id,
1661                finish_free.first_edge_id,
1662                start_first.is_consumed(),
1663                start_first.existing().unwrap_or(0),
1664                finish_first.is_consumed(),
1665                finish_first.existing().unwrap_or(0),
1666                cr,
1667                blend_cr_forward,
1668                band,
1669            )?,
1670            detect_row_coincidence(
1671                solid,
1672                second,
1673                edge.id,
1674                start_free.second_edge_id,
1675                finish_free.second_edge_id,
1676                start_second.is_consumed(),
1677                start_second.existing().unwrap_or(0),
1678                finish_second.is_consumed(),
1679                finish_second.existing().unwrap_or(0),
1680                cs,
1681                !blend_cr_forward,
1682                band,
1683            )?,
1684        ),
1685        _ => (None, None),
1686    };
1687    // One face hosting both sides (the blended edge used twice by one face)
1688    // cannot be dropped for one side while the other still splices into it —
1689    // ambiguous, keep the pristine path for both.
1690    let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1691        if first.face.id == second.face.id {
1692            (None, None)
1693        } else {
1694            (sew_first, sew_second)
1695        };
1696
1697    // Resolve the four rim vertices: reuse the existing vertex for a consumed
1698    // side, else create a fresh vertex at the support-row endpoint.
1699    let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1700        match rim.existing() {
1701            Some(id) => Ok(id),
1702            None => {
1703                let id = take_id();
1704                result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1705                Ok(id)
1706            }
1707        }
1708    };
1709    let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1710    let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1711    let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1712    let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1713    // How far the consumed rims and sewn rows moved a rail onto what already
1714    // existed — what `check_snap_closure` measures the body against.
1715    let mut snap = [&sew_first, &sew_second]
1716        .into_iter()
1717        .flatten()
1718        .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1719    for (rim, vertex, row_end) in [
1720        (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1721        (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1722        (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1723        (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1724    ] {
1725        if !rim.is_consumed() {
1726            continue;
1727        }
1728        let point = result
1729            .vertices
1730            .iter()
1731            .find(|candidate| candidate.id == vertex)
1732            .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1733            .point;
1734        snap = snap.max(row_end.sub(point).length());
1735    }
1736
1737    // Support edges: a sewn side reuses the EXISTING coincident edge (no
1738    // fresh edge — OCCT's `SetExistingEdge` move); a pristine side gets the
1739    // fitted row as a fresh edge.
1740    // A collapsed side builds no edge at all.
1741    let cr_edge_id = match (&sew_first, &cr_rail) {
1742        (Some(sew), _) => Some(sew.edge_id),
1743        (None, None) => None,
1744        (None, Some((cr, _))) => {
1745            let id = take_id();
1746            result.edges.push(EdgeRecord {
1747                id,
1748                curve: cr.clone(),
1749                t0: cr_domain[0],
1750                t1: cr_domain[1],
1751                start_vertex_id: w1a,
1752                end_vertex_id: w1b,
1753                degenerate: false,
1754                name: None,
1755            });
1756            Some(id)
1757        }
1758    };
1759    let cs_edge_id = match (&sew_second, &cs_rail) {
1760        (Some(sew), _) => Some(sew.edge_id),
1761        (None, None) => None,
1762        (None, Some((cs, _))) => {
1763            let id = take_id();
1764            result.edges.push(EdgeRecord {
1765                id,
1766                curve: cs.clone(),
1767                t0: cs_domain[0],
1768                t1: cs_domain[1],
1769                start_vertex_id: w2a,
1770                end_vertex_id: w2b,
1771                degenerate: false,
1772                name: None,
1773            });
1774            Some(id)
1775        }
1776    };
1777    // The edge closing each end of the blend face: the §6.9 transverse curve
1778    // on the end face for a free end, or — at a corner — the end
1779    // cross-section arc the corner patch was already given, committed by the
1780    // caller and merely referenced here.
1781    let mut commit_end_edge = |plan: &EndPlan,
1782                               first_rim: u64,
1783                               second_rim: u64|
1784     -> Result<Option<u64>, KernelRefusal> {
1785        match plan {
1786            EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1787            EndPlan::Free(free) => {
1788                let id = take_id();
1789                let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1790                result.edges.push(EdgeRecord {
1791                    id,
1792                    curve: free.transverse_curve.clone(),
1793                    t0: domain[0],
1794                    t1: domain[1],
1795                    start_vertex_id: first_rim,
1796                    end_vertex_id: second_rim,
1797                    degenerate: false,
1798                    name: None,
1799                });
1800                Ok(Some(id))
1801            }
1802        }
1803    };
1804    let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
1805    let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
1806
1807    // Replace the blended edge in each PRISTINE mate's loop — the blend
1808    // face's loop direction is forced by manifold pairing with F1's use of
1809    // the blended edge (`first_use_forward`, captured above).  A sewn mate is
1810    // dropped whole below; nothing to splice.  A COLLAPSED side has no rail to
1811    // put there: the mate simply loses the blended edge, and its loop closes
1812    // through the rim vertices the orchestrator identifies.
1813    for (mate, rail, sewn) in [
1814        (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1815        (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1816    ] {
1817        if sewn {
1818            continue;
1819        }
1820        let face = result
1821            .shells
1822            .iter_mut()
1823            .flat_map(|shell| &mut shell.faces)
1824            .find(|face| face.id == mate.face.id)
1825            .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1826        let loop_record = &mut face.loops[mate.loop_index];
1827        let position = loop_record
1828            .coedges
1829            .iter()
1830            .position(|coedge| coedge.edge_id == edge.id)
1831            .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1832        let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1833            loop_record.coedges.remove(position);
1834            continue;
1835        };
1836        let old_forward = loop_record.coedges[position].forward;
1837        loop_record.coedges[position] = CoedgeRecord {
1838            id: loop_record.coedges[position].id,
1839            edge_id: new_edge_id,
1840            forward: old_forward,
1841            pcurve: if old_forward {
1842                pcurve_forward.clone()
1843            } else {
1844                pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1845            },
1846        };
1847    }
1848
1849    // A capped end's legs: on each mate, the leg runs from the rail's rim
1850    // vertex to the sharp vertex, where the continuing edge still starts.
1851    // It goes between the rail coedge and that continuing coedge, with the
1852    // sense that walks rim -> vertex when the rail ends at the rim.
1853    for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
1854        let EndPlan::Cap(cap) = plan else {
1855            continue;
1856        };
1857        for (mate, rail_edge_id, leg, rim) in [
1858            (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
1859            (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
1860        ] {
1861            // Refused above: a capped end never meets a collapsed rail.
1862            let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
1863            let face = result
1864                .shells
1865                .iter_mut()
1866                .flat_map(|shell| &mut shell.faces)
1867                .find(|face| face.id == mate.face.id)
1868                .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
1869            let loop_record = &mut face.loops[mate.loop_index];
1870            let count = loop_record.coedges.len();
1871            let rail_at = loop_record
1872                .coedges
1873                .iter()
1874                .position(|coedge| coedge.edge_id == rail_edge_id)
1875                .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
1876            // Which side of the rail coedge is this end?  The rail coedge
1877            // traverses rim-to-rim; the capped end is at its traversal END
1878            // when walking forward means station order and this is the
1879            // finish end, etc.  Decide by geometry: the neighbour whose
1880            // traversal touches the sharp vertex.
1881            let rail_forward = loop_record.coedges[rail_at].forward;
1882            let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
1883            let (insert_at, forward, pcurve) = if end_is_traversal_end {
1884                // rail ... rim -> [leg rim->vertex] -> continuing edge
1885                (rail_at + 1, true, leg.1.clone())
1886            } else {
1887                // continuing edge -> [leg vertex->rim] -> rim ... rail
1888                (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
1889            };
1890            let _ = (count, rim);
1891            loop_record.coedges.insert(
1892                insert_at,
1893                CoedgeRecord {
1894                    id: take_id(),
1895                    edge_id: leg.0,
1896                    forward,
1897                    pcurve,
1898                },
1899            );
1900        }
1901    }
1902
1903    // Drop consumed boundary edges from the mate loop they share with the
1904    // blended edge: the fresh support coedge already begins at the reused far
1905    // vertex, so removing the fully-covered boundary keeps the loop closed.
1906    // A SEWN mate's whole loop collapses (the face is dropped below), so its
1907    // consumed boundaries are only recorded for deletion, never spliced.
1908    let mut consumed_edges: Vec<u64> = Vec::new();
1909    let start_free = start_end.free();
1910    let finish_free = finish_end.free();
1911    for (mate, sewn, sides) in [
1912        (
1913            first,
1914            sew_first.is_some(),
1915            [
1916                (&start_first, start_free.map(|free| free.first_edge_id)),
1917                (&finish_first, finish_free.map(|free| free.first_edge_id)),
1918            ],
1919        ),
1920        (
1921            second,
1922            sew_second.is_some(),
1923            [
1924                (&start_second, start_free.map(|free| free.second_edge_id)),
1925                (&finish_second, finish_free.map(|free| free.second_edge_id)),
1926            ],
1927        ),
1928    ] {
1929        for (cross, boundary_id) in sides {
1930            if !cross.is_consumed() {
1931                continue;
1932            }
1933            let Some(boundary_id) = boundary_id else {
1934                continue;
1935            };
1936            if !sewn {
1937                let face = result
1938                    .shells
1939                    .iter_mut()
1940                    .flat_map(|shell| &mut shell.faces)
1941                    .find(|face| face.id == mate.face.id)
1942                    .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1943                face.loops[mate.loop_index]
1944                    .coedges
1945                    .retain(|coedge| coedge.edge_id != boundary_id);
1946            }
1947            if !consumed_edges.contains(&boundary_id) {
1948                consumed_edges.push(boundary_id);
1949            }
1950        }
1951    }
1952    // Lesson 7: zero-span leftovers of a collapsing loop go with their face.
1953    for sew in [&sew_first, &sew_second].into_iter().flatten() {
1954        for id in &sew.collapsed_edges {
1955            if !consumed_edges.contains(id) {
1956                consumed_edges.push(*id);
1957            }
1958        }
1959    }
1960
1961    // Locate the corner junction on each end face BEFORE trimming (the two
1962    // boundary coedges that meet at the old corner vertex, stepping over the
1963    // pole between them where the end face's apex IS that vertex) — edge ids
1964    // alone are ambiguous on two-coedge cap loops, so the meeting must be at
1965    // the corner.
1966    // (end, transverse_id, corner, first_consumed, second_consumed) -> plan.
1967    // Only FREE ends appear here: a corner stop rebuilds nothing on a third
1968    // face, because the corner patch is what closes it.
1969    let mut end_plans = Vec::with_capacity(2);
1970    for (end, transverse_id, corner, first_consumed, second_consumed) in [
1971        (
1972            start_end.free(),
1973            transverse_a_id,
1974            old_start,
1975            start_first.is_consumed(),
1976            start_second.is_consumed(),
1977        ),
1978        (
1979            finish_end.free(),
1980            transverse_b_id,
1981            old_finish,
1982            finish_first.is_consumed(),
1983            finish_second.is_consumed(),
1984        ),
1985    ] {
1986        let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
1987            continue;
1988        };
1989        let face = result
1990            .shells
1991            .iter()
1992            .flat_map(|shell| &shell.faces)
1993            .find(|face| face.id == end.end_face_id)
1994            .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
1995        // Key on stable coedge ids (not indices) so processing one end can't
1996        // invalidate another that shares this face.
1997        let Some(located) = locate_end_corner(
1998            &result,
1999            face,
2000            corner,
2001            end.first_edge_id,
2002            end.second_edge_id,
2003        ) else {
2004            return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
2005        };
2006        // The apex the transverse curve cuts off goes with the boundaries the
2007        // blend consumes.
2008        for pole in &located.pole_edge_ids {
2009            if !consumed_edges.contains(pole) {
2010                consumed_edges.push(*pole);
2011            }
2012        }
2013        end_plans.push((
2014            end.end_face_id,
2015            transverse_id,
2016            end.transverse_end_pcurve.clone(),
2017            located,
2018            first_consumed,
2019            second_consumed,
2020        ));
2021    }
2022
2023    // Trim the boundary edges at their support crossings.  A consumed side is
2024    // deleted instead of trimmed; a CORNER side touches no boundary at all.
2025    for (rim_resolution, boundary, corner, rim) in [
2026        (
2027            &start_first,
2028            start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2029            old_start,
2030            w1a,
2031        ),
2032        (
2033            &start_second,
2034            start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2035            old_start,
2036            w2a,
2037        ),
2038        (
2039            &finish_first,
2040            finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2041            old_finish,
2042            w1b,
2043        ),
2044        (
2045            &finish_second,
2046            finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2047            old_finish,
2048            w2b,
2049        ),
2050    ] {
2051        if !rim_resolution.trims_boundary() {
2052            continue;
2053        }
2054        let Some((boundary_id, boundary_param)) = boundary else {
2055            continue;
2056        };
2057        trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
2058    }
2059
2060    // Rebuild each end face's corner: drop the consumed boundary coedge(s) and
2061    // splice in the transverse coedge between the (trimmed) boundaries.
2062    for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
2063        end_plans
2064    {
2065        let forward = located.x_is_first;
2066        let pcurve = if forward {
2067            transverse_end_pcurve
2068        } else {
2069            transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2070        };
2071        let (x_consumed, y_consumed) = if located.x_is_first {
2072            (first_consumed, second_consumed)
2073        } else {
2074            (second_consumed, first_consumed)
2075        };
2076        let transverse_coedge = CoedgeRecord {
2077            id: take_id(),
2078            edge_id: transverse_id,
2079            forward,
2080            pcurve,
2081        };
2082        splice_end_corner(
2083            result,
2084            end_face,
2085            &located,
2086            transverse_coedge,
2087            x_consumed,
2088            y_consumed,
2089        )?;
2090    }
2091
2092    // Blend face: cr fwd -> TB -> cs rev -> TA rev in (t, z) space.  Both rows
2093    // share one parameterisation, so a collapsed cr reads its sense across the
2094    // strip the cs rail still spans (a stripe with BOTH rails collapsed has no
2095    // area; the orchestrator drops it with the edges that bound it).
2096    let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2097        (Some((_, cr_pcurve)), _) => {
2098            let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2099            (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2100        }
2101        (None, Some(_)) => {
2102            let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2103            (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2104        }
2105        (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2106    };
2107    let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2108    let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2109    let out1 = if first.face.same_sense {
2110        n1
2111    } else {
2112        n1.scale(-1.0)
2113    };
2114    let same_sense = blend_normal.dot(out1) >= 0.0;
2115    let loop_id = take_id();
2116    // Traversal senses of the blend's support coedges.  A fresh support edge
2117    // is parameterized in station order, so the sense is the loop's walking
2118    // direction (`blend_cr_forward` for cr, its opposite for cs); a SEWN
2119    // side's sense comes from the detection (the existing edge's own
2120    // parameter direction relative to that same walk — equal to the dropped
2121    // face's old sense, preserving manifold pairing with the survivor).  The
2122    // pcurves always follow the LOOP's walking direction and are unchanged.
2123    let cr_forward = sew_first
2124        .as_ref()
2125        .map_or(blend_cr_forward, |sew| sew.forward);
2126    let cs_forward = sew_second
2127        .as_ref()
2128        .map_or(!blend_cr_forward, |sew| sew.forward);
2129    // The two end slots of the blend loop.  `natural_forward` is the sense the
2130    // loop walks a transverse edge stored first-rim -> second-rim; a CORNER
2131    // arc was already committed in the walk direction (so the corner patch can
2132    // take it the other way round), so it is always used forward.
2133    // A slot is one coedge for a free end (the transverse curve) or a corner
2134    // end (the section arc), and one or two for a miter end (the seam, then
2135    // the sibling's section when the seam left the sibling first).  A corner
2136    // arc was committed in the walk direction; a miter edge records its own
2137    // sense along the walk.
2138    let mut end_slot = |plan: &EndPlan,
2139                        free_edge_id: Option<u64>,
2140                        natural_forward: bool|
2141     -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2142        match plan {
2143            EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2144                id: take_id(),
2145                edge_id: end.arc_edge_id,
2146                forward: true,
2147                pcurve: end.arc_blend_pcurve.clone(),
2148            }]),
2149            EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2150                id: take_id(),
2151                edge_id: end.arc_edge_id,
2152                forward: true,
2153                pcurve: end.arc_blend_pcurve.clone(),
2154            }]),
2155            EndPlan::Miter(end) => end
2156                .edges
2157                .iter()
2158                .map(|(edge_id, forward, pcurve)| {
2159                    Ok(CoedgeRecord {
2160                        id: take_id(),
2161                        edge_id: *edge_id,
2162                        forward: *forward,
2163                        pcurve: pcurve.clone(),
2164                    })
2165                })
2166                .collect(),
2167            EndPlan::Free(free) => {
2168                let edge_id =
2169                    free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2170                Ok(vec![if natural_forward {
2171                    CoedgeRecord {
2172                        id: take_id(),
2173                        edge_id,
2174                        forward: true,
2175                        pcurve: free.transverse_blend_pcurve.clone(),
2176                    }
2177                } else {
2178                    CoedgeRecord {
2179                        id: take_id(),
2180                        edge_id,
2181                        forward: false,
2182                        pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2183                    }
2184                }])
2185            }
2186        }
2187    };
2188    let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2189    let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2190    // A collapsed rail has no coedge: the end slots on either side of it meet
2191    // at its one rim point.
2192    let coedges = if blend_cr_forward {
2193        let mut coedges = Vec::new();
2194        if let Some(cr_edge_id) = cr_edge_id {
2195            coedges.push(CoedgeRecord {
2196                id: take_id(),
2197                edge_id: cr_edge_id,
2198                forward: cr_forward,
2199                pcurve: crate::sweep_topology::parameter_line(
2200                    cr_domain[0],
2201                    0.0,
2202                    cr_domain[1],
2203                    0.0,
2204                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2205            });
2206        }
2207        coedges.extend(finish_slot);
2208        if let Some(cs_edge_id) = cs_edge_id {
2209            coedges.push(CoedgeRecord {
2210                id: take_id(),
2211                edge_id: cs_edge_id,
2212                forward: cs_forward,
2213                pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2214            });
2215        }
2216        coedges.extend(start_slot);
2217        coedges
2218    } else {
2219        let mut coedges = Vec::new();
2220        if let Some(cr_edge_id) = cr_edge_id {
2221            coedges.push(CoedgeRecord {
2222                id: take_id(),
2223                edge_id: cr_edge_id,
2224                forward: cr_forward,
2225                pcurve: crate::sweep_topology::parameter_line(
2226                    cr_domain[1],
2227                    0.0,
2228                    cr_domain[0],
2229                    0.0,
2230                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2231            });
2232        }
2233        coedges.extend(start_slot);
2234        if let Some(cs_edge_id) = cs_edge_id {
2235            coedges.push(CoedgeRecord {
2236                id: take_id(),
2237                edge_id: cs_edge_id,
2238                forward: cs_forward,
2239                pcurve: crate::sweep_topology::parameter_line(
2240                    cs_domain[0],
2241                    1.0,
2242                    cs_domain[1],
2243                    1.0,
2244                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2245            });
2246        }
2247        coedges.extend(finish_slot);
2248        coedges
2249    };
2250    let blend_face_id = take_id();
2251    let mut blend_face = FaceRecord {
2252        id: blend_face_id,
2253        surface: rows.surface,
2254        same_sense,
2255        loops: vec![LoopRecord {
2256            id: loop_id,
2257            coedges,
2258        }],
2259        name: name.map(|value| value.to_string()),
2260    };
2261    if rows.exact_extrusion {
2262        transpose_face(&mut blend_face)?;
2263    }
2264    let shell_index = result
2265        .shells
2266        .iter()
2267        .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2268        .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2269    // Drop the fully-consumed mate faces (lesson 3): their loops collapsed
2270    // between the sewn edge and the blend; the consumed boundaries and
2271    // lesson-7 leftovers are deleted below, the sewn edge lives on shared by
2272    // the blend face and the surviving neighbour.
2273    for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2274        if sew.is_some() {
2275            for shell in &mut result.shells {
2276                shell.faces.retain(|face| face.id != mate.face.id);
2277            }
2278        }
2279    }
2280    result.shells[shell_index].faces.push(blend_face);
2281
2282    result.edges.retain(|candidate| candidate.id != edge.id);
2283    // Delete the boundary edges a prior fillet's corner had left behind that
2284    // this blend fully consumed.
2285    result
2286        .edges
2287        .retain(|candidate| !consumed_edges.contains(&candidate.id));
2288    // Each collapsed rail's two rims are one vertex.
2289    let mut identified = Vec::new();
2290    for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2291        if collapsed && from != to {
2292            identified.push((from, to));
2293        }
2294    }
2295    Ok(SewnStripe {
2296        cr_edge_id,
2297        cs_edge_id,
2298        blend_face_id,
2299        consumed: [sew_first.is_some(), sew_second.is_some()],
2300        snap,
2301        identified,
2302    })
2303}
2304
2305/// Swap the two parameters of a face: the surface's control net, every
2306/// coedge pcurve, and the sense (S_u × S_v changes sign).  The face's
2307/// geometry is untouched; only its chart is.  Used to hand an exact
2308/// cylinder patch built with its section along v to the analytic recogniser,
2309/// which wants the circle along u.
2310pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2311    let surface = &face.surface;
2312    let rows_u = surface.control_points.len();
2313    let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2314    let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2315    for row in &surface.control_points {
2316        for (j, point) in row.iter().enumerate() {
2317            transposed[j].push(*point);
2318        }
2319    }
2320    face.surface = NurbsSurface::new(
2321        surface.degree_v,
2322        surface.degree_u,
2323        surface.knots_v.clone(),
2324        surface.knots_u.clone(),
2325        transposed,
2326    ).or_refuse(KernelStage::Refine, "new")?;
2327    for loop_record in &mut face.loops {
2328        for coedge in &mut loop_record.coedges {
2329            for control in &mut coedge.pcurve.control_points {
2330                std::mem::swap(&mut control.x, &mut control.y);
2331            }
2332        }
2333    }
2334    face.same_sense = !face.same_sense;
2335    Ok(())
2336}
2337
2338/// Drop every vertex no longer referenced by an edge — the sharp corners the
2339/// blends replaced, and anything freed by a consumed boundary.  Run ONCE after
2340/// a whole group of stripes is in: a corner vertex is still referenced by the
2341/// selected edges that have not been sewn yet.
2342pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2343    let used: rustc_hash::FxHashSet<u64> = result
2344        .edges
2345        .iter()
2346        .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2347        .collect();
2348    result
2349        .vertices
2350        .retain(|candidate| used.contains(&candidate.id));
2351}
2352
2353fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2354    crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2355}
2356
2357