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
598pub(in crate::blend) fn affine_reads_on() -> bool {
599    switch!(live "BREP_BLEND_AFFINE_READS", AFFINE_READS_DEFAULT)
600}
601
602
603/// A point of `surface` and its unit normal when the surface is AFFINE in
604/// (u, v), its extension included: a plane (`analytic`) of degree 1 × 1 whose
605/// 2 × 2 net is a parallelogram with equal weights, to `1e-12·(1 + scale)`.
606/// Every point of such a surface, and of its linear extension, lies on the one
607/// plane through the corner with that normal.
608fn affine_plane(surface: &NurbsSurface) -> Option<(Vec3, Vec3)> {
609    let (origin, _, _) = affine_plane_frame(surface)?;
610    // The normal off the net's own corner differences.
611    let net = &surface.control_points;
612    let [p00, p01, p10] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?];
613    Some((origin, p10.sub(p00).cross(p01.sub(p00)).normalized().ok()?))
614}
615
616/// [`affine_plane`]'s surface as `S(u, v) = origin + (u − u₀)·s_u + (v − v₀)·s_v`:
617/// the net's corner and the two constant partial derivatives.
618pub(in crate::blend) fn affine_plane_frame(surface: &NurbsSurface) -> Option<(Vec3, Vec3, Vec3)> {
619    if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || surface.degree_u != 1 || surface.degree_v != 1 {
620        return None;
621    }
622    let net = &surface.control_points;
623    if net.len() != 2 || net.iter().any(|row| row.len() != 2) {
624        return None;
625    }
626    let weight = net[0][0].w;
627    if !(weight > 0.0) || net.iter().flatten().any(|point| point.w != weight) {
628        return None;
629    }
630    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()?];
631    let scale = [p00, p01, p10, p11].iter().fold(0.0f64, |worst, point| worst.max(point.length()));
632    if p11.sub(p10).sub(p01.sub(p00)).length() > 1e-12 * (1.0 + scale) {
633        return None;
634    }
635    let [u0, u1] = surface.domain_u().ok()?;
636    let [v0, v1] = surface.domain_v().ok()?;
637    if !(u1 > u0 && v1 > v0) {
638        return None;
639    }
640    Some((p00, p10.sub(p00).scale(1.0 / (u1 - u0)), p01.sub(p00).scale(1.0 / (v1 - v0))))
641}
642
643/// Whether `curve` is AFFINE in its parameter: degree 1, two control points of
644/// equal weight (a `make_line`).
645fn affine_curve(curve: &NurbsCurve) -> bool {
646    curve.degree == 1
647        && curve.control_points.len() == 2
648        && curve.control_points[0].w > 0.0
649        && curve.control_points[0].w == curve.control_points[1].w
650}
651
652/// The worst distance of an affine rail from an affine carrier over `window`,
653/// read at the window's two ends: the signed distance is affine in the rail
654/// parameter, so its magnitude is convex and its maximum is at an end. `None`
655/// when either is not affine (the sampled reader's), or an end will not
656/// evaluate.
657pub(in crate::blend) fn affine_rail_standoff(rail: &NurbsCurve, carrier: &NurbsSurface, window: [f64; 2]) -> Option<f64> {
658    if !affine_curve(rail) {
659        return None;
660    }
661    let (origin, normal) = affine_plane(carrier)?;
662    let mut worst = 0.0f64;
663    for u in window {
664        let point = rail.evaluate(u).ok()?;
665        let off = point.sub(origin).dot(normal).abs();
666        if !off.is_finite() {
667            return None;
668        }
669        worst = worst.max(off);
670    }
671    Some(worst)
672}
673
674/// Lane N row 4 (2026-10-28): the exact revolution rows' reads in closed
675/// form. `BREP_EXACT_REVOLUTION_READS=1`/`on` turns them on, `=0`/`off`
676/// off; ON by default since lane N's promotion (2026-10-28).
677const EXACT_REVOLUTION_READS_DEFAULT: bool = true;
678
679pub(in crate::blend) fn exact_revolution_reads_on() -> bool {
680    switch!(live "BREP_EXACT_REVOLUTION_READS", EXACT_REVOLUTION_READS_DEFAULT)
681}
682
683
684/// A bound on `|S(pcurve(s)) − rail(s)|` over the whole parameter range when
685/// the shipped pair is an exact revolution row's (`exact_revolution_rows`):
686///
687/// * on the REVOLUTION carrier the pcurve is a uv line along one coordinate
688///   and the rail is the carrier's parallel circle at the held coordinate,
689///   continued to the line's range along its own conic pieces
690///   (`extend_conic_ends`) and re-parameterised to `[0, 1]`; that curve is
691///   rebuilt here from the pcurve's two ends and compared with the rail net
692///   for net — identical bases, so the homogeneous control differences
693///   bound the curves everywhere (`net_bound`);
694/// * on an AFFINE plane the pcurve carries the rail's own knots and the
695///   chart images of the rail's control points; the plane's image of the
696///   pcurve net is compared with the rail net the same way.
697///
698/// `None` when the pair is neither (the sampled read runs). The rail's
699/// standoff from the carrier and the pcurve's miss are each at most the
700/// bound (a point's distance to a surface is at most its distance to any
701/// point of it; the miss adds the standoff once more), so a bound within a
702/// third of the pcurve floor stands the trim by the march trim's own rule.
703pub(in crate::blend) fn exact_revolution_read_bound(rail: &NurbsCurve, carrier: &NurbsSurface, pcurve: &NurbsCurve) -> Option<f64> {
704    if let Some((origin, s_u, s_v)) = affine_plane_frame(carrier) {
705        // The plane's image of the pcurve net, weight for weight.
706        if pcurve.degree != rail.degree || pcurve.knots != rail.knots || pcurve.control_points.len() != rail.control_points.len() {
707            return None;
708        }
709        let ([u0, _], [v0, _]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
710        let image: Vec<Vec4> = pcurve
711            .control_points
712            .iter()
713            .map(|control| {
714                let (u, v) = (control.x / control.w, control.y / control.w);
715                let point = origin.add(s_u.scale(u - u0)).add(s_v.scale(v - v0));
716                Vec4 { x: point.x * control.w, y: point.y * control.w, z: point.z * control.w, w: control.w }
717            })
718            .collect();
719        return crate::blend::stations::net_bound(&rail.control_points, &image);
720    }
721    if !matches!(carrier.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. } | crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. } | crate::AnalyticSurface::Sphere { .. })) {
722        return None;
723    }
724    if !affine_curve(pcurve) {
725        return None;
726    }
727    let [q0, q1] = pcurve.domain().ok()?;
728    let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
729    let ([u0, u1], [v0, v1]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
730    let (circle, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x && a.y >= v0 && a.y <= v1 {
731        (carrier.iso_curve_v(a.y).ok()?, a.x, b.x)
732    } else if a.x.to_bits() == b.x.to_bits() && a.y != b.y && a.x >= u0 && a.x <= u1 {
733        (carrier.iso_curve_u(a.x).ok()?, a.y, b.y)
734    } else {
735        return None;
736    };
737    let [c0, c1] = circle.domain().ok()?;
738    let reversed = to < from;
739    let circle = if reversed { circle.reversed().ok()? } else { circle };
740    let on_curve = |along: f64| if reversed { c0 + c1 - along } else { along };
741    let (lo, hi) = (on_curve(from), on_curve(to));
742    if !(hi > lo) {
743        return None;
744    }
745    let segment = super::support::extend_conic_ends(&circle, lo, hi).ok()?;
746    let [r0, r1] = segment.domain().ok()?;
747    let degree = segment.degree;
748    let mut knots = segment.knots.clone();
749    for knot in knots.iter_mut() {
750        *knot = ((*knot - r0) / (r1 - r0)).clamp(0.0, 1.0);
751    }
752    for knot in knots.iter_mut().take(degree + 1) {
753        *knot = 0.0;
754    }
755    let count = knots.len();
756    for knot in knots.iter_mut().skip(count - degree - 1) {
757        *knot = 1.0;
758    }
759    if rail.degree != degree || rail.knots != knots || rail.control_points.len() != segment.control_points.len() {
760        return None;
761    }
762    crate::blend::stations::net_bound(&rail.control_points, &segment.control_points)
763}
764
765/// Each shipped rail's worst distance from its carrier over ITS OWN window
766/// ([`shipped_window`]): both window ends exactly and the quarter points of
767/// every station span inside it, each against the carrier's EXTENSION
768/// (`extended_foot_distance`, seeded by the stations' own contact uv) --
769/// a finite planar chart clamps its projection, so retained overshoot past a
770/// vertex would read a miss that is not there. `(worst, samples per rail)`;
771/// any unreadable sample (a failed evaluation, an infinite foot) makes the
772/// worst NaN, which [`OpenRungReading::rails_read`] never admits.
773pub(in crate::blend) fn rails_on_shipped_windows(
774    rows: &FittedRows,
775    parameters: &[f64],
776    stations: &[Station],
777    carriers: [&NurbsSurface; 2],
778    windows: [Option<[f64; 2]>; 2],
779) -> (f64, [usize; 2]) {
780    read_rail_windows([&rows.cr, &rows.cs], parameters, stations, carriers, windows,
781        rows.exact_extrusion && affine_reads_on(), certified_rail_bounds(rows, carriers), None)
782}
783
784/// An exact revolution row's rails, bounded in closed form
785/// (`exact_revolution_read_bound`) when the reads lane is on: the standoff
786/// over any window is at most the bound.
787fn certified_rail_bounds(rows: &FittedRows, carriers: [&NurbsSurface; 2]) -> [Option<f64>; 2] {
788    if rows.revolution_axis.is_none() || !exact_revolution_reads_on() {
789        return [None, None];
790    }
791    [
792        exact_revolution_read_bound(&rows.cr, carriers[0], &rows.cr_pcurve),
793        exact_revolution_read_bound(&rows.cs, carriers[1], &rows.cs_pcurve),
794    ]
795}
796
797fn read_rail_windows(
798    rails: [&NurbsCurve; 2], parameters: &[f64], stations: &[Station],
799    carriers: [&NurbsSurface; 2], windows: [Option<[f64; 2]>; 2], affine: bool,
800    certified: [Option<f64>; 2],
801    mut memo: Option<&mut RailFeet>,
802) -> (f64, [usize; 2]) {
803    let mut worst = 0.0_f64;
804    let mut samples = [0usize; 2];
805    for (side, (rail, carrier)) in [(rails[0], carriers[0]), (rails[1], carriers[1])].into_iter().enumerate() {
806        let Some([low, high]) = windows[side] else { continue };
807        // An exact extrusion's straight rail on a plane: the ends carry the
808        // worst (`affine_rail_standoff`), the samples between them cannot.
809        if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
810            crate::blend::record_count("count.read.affine_rails", 1);
811            samples[side] += 2;
812            if !worst.is_nan() && off > worst {
813                worst = off;
814            }
815            continue;
816        }
817        // An exact revolution row's rail: its net bound against the carrier's
818        // own circle (or the plane's image of its pcurve) is its standoff's
819        // bound over every window.
820        if let Some(bound) = certified[side] {
821            crate::blend::record_count("count.read.revolution_rails", 1);
822            samples[side] += 1;
823            if !worst.is_nan() && bound > worst {
824                worst = bound;
825            }
826            continue;
827        }
828        let mut at = vec![low, high];
829        for pair in parameters.windows(2) {
830            for fraction in [0.25, 0.5, 0.75] {
831                let u = pair[0] + (pair[1] - pair[0]) * fraction;
832                if u > low && u < high {
833                    at.push(u);
834                }
835            }
836        }
837        for u in at {
838            let cached = memo.as_ref().and_then(|memo| memo.values[side].get(&u.to_bits()).copied());
839            let off = if let Some(off) = cached {
840                if let Some(memo) = memo.as_mut() { memo.hits += 1; }
841                crate::blend::record_count("count.read.rail_foot_reused", 1);
842                off
843            } else {
844            if let Some(memo) = memo.as_mut() {
845                memo.reads += 1;
846                crate::blend::record_count("count.read.rail_foot_fresh", 1);
847            }
848            let off = interval_at(parameters, u)
849                .and_then(|(interval, fraction)| {
850                    let (left, right) = (&stations[interval], &stations[interval + 1]);
851                    let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
852                    let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
853                    let point = rail.evaluate(u).ok()?;
854                    crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
855                })
856                .filter(|off| off.is_finite())
857                .unwrap_or(f64::NAN);
858            if off.is_finite() {
859                if let Some(memo) = memo.as_mut() { memo.values[side].insert(u.to_bits(), off); }
860            }
861            off
862            };
863            samples[side] += 1;
864            if !worst.is_nan() && (off.is_nan() || off > worst) {
865                worst = off;
866            }
867        }
868    }
869    (worst, samples)
870}
871
872/// A construction-local rail reader. It owns an immutable snapshot of every
873/// input to a foot reading; only the requested windows can change. Native
874/// parameter bits identify a sample, so nearby endpoints never share a foot.
875/// The network keeps this reader from its chosen rung through shipped-window
876/// acceptance, then drops it. Failed/nonfinite feet are never reused.
877pub(in crate::blend) struct RailWindowReader {
878    rails: [NurbsCurve; 2],
879    carriers: [NurbsSurface; 2],
880    parameters: Vec<f64>,
881    stations: Vec<Station>,
882    affine: bool,
883    certified: [Option<f64>; 2],
884    feet: RailFeet,
885}
886
887#[derive(Default)]
888struct RailFeet {
889    values: [rustc_hash::FxHashMap<u64, f64>; 2],
890    reads: u64,
891    hits: u64,
892}
893
894impl RailWindowReader {
895    pub(in crate::blend) fn new(rows: &FittedRows, parameters: &[f64], stations: &[Station], carriers: [&NurbsSurface; 2]) -> Self {
896        Self {
897            rails: [rows.cr.clone(), rows.cs.clone()],
898            carriers: [carriers[0].clone(), carriers[1].clone()],
899            parameters: parameters.to_vec(), stations: stations.to_vec(),
900            affine: rows.exact_extrusion && affine_reads_on(), certified: certified_rail_bounds(rows, carriers), feet: RailFeet::default(),
901        }
902    }
903
904    pub(in crate::blend) fn read(&mut self, windows: [Option<[f64; 2]>; 2]) -> (f64, [usize; 2]) {
905        read_rail_windows([&self.rails[0], &self.rails[1]], &self.parameters, &self.stations,
906            [&self.carriers[0], &self.carriers[1]], windows, self.affine, self.certified, Some(&mut self.feet))
907    }
908}
909
910pub(in crate::blend) fn network_rail_reuse_on() -> bool {
911    // The private halving diagnostic compares every requested foot against
912    // its reference algorithm, including repeats. Preserve that coverage.
913    std::env::var("BREP_NETWORK_RAIL_REUSE").map_or(true, |value| value != "0" && value != "off")
914}
915
916/// The JUNCTION stencil across the station between interval `left` and
917/// `left + 1`: `left_nodes` (33 exact centres of `left`) from node 16 to 32,
918/// then `right_nodes` (33 of `left + 1`) from node 1 to 16, the shared
919/// station at node 16. Its native abscissae come from `station_ts` (the
920/// edge parameters the centres were solved at): `None` when the two native
921/// widths are bitwise equal -- uniform nodes, read by the ordinary reader --
922/// else each node's t, interior nodes by the dense solves' own arithmetic
923/// t_i + (t_{i+1} − t_i)·j/32 and the shared station at its STORED t. Err
924/// when a station t is not finite or a width is not strictly positive.
925pub(in crate::blend) fn junction_stencil(
926    left_nodes: &[Vec3],
927    right_nodes: &[Vec3],
928    station_ts: &[f64],
929    left: usize,
930) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
931    let half = OPEN_WALL_CENTRES / 2;
932    if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
933        return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
934    }
935    let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
936    let (w_left, w_right) = (t1 - t0, t2 - t1);
937    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()) {
938        return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
939    }
940    let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
941    if w_left.to_bits() == w_right.to_bits() {
942        return Ok((nodes, None));
943    }
944    let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
945        .map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
946        .chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
947        .collect();
948    Ok((nodes, Some(abscissae)))
949}
950
951/// One wall probe on a stencil: the ordinary uniform reader when it has no
952/// native abscissae (an interval's own nodes, or a junction of equal native
953/// widths), the native-abscissa reader when it has them.
954pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
955    match abscissae {
956        None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
957        Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
958    }
959}
960
961/// Every interval's largest DECLARED wall reading (`chain::declared_wall_probe`,
962/// reading + uncertainty) on the open fillet `rows`, against the exact ball
963/// centres the march solved at j/32 of each interval ([`OpenDenseCentres`]),
964/// or, for each unread interval, WHY: its station
965/// interval and which component failed (no dense row, a dense row of the
966/// wrong size, an exact centre the march did not solve, a wall point the
967/// surface would not evaluate, a probe the declared reader declined, no probe
968/// in the shipped part, or a non-finite reading). The cause is diagnostic.
969///
970/// Read on the DECLARED SHIPPED REGION `window` only: an interval it overlaps
971/// is probed at its even stencil fractions that lie inside the window and at
972/// each window end inside the interval; a probe outside the window is not on
973/// the shipped wall and is not read. A window END whose nearest even stencil
974/// node falls outside 2..=30 -- an end within a node of the interval's own
975/// station, as the original bore's shipped spans were, 1e-16 from their
976/// vertex stations -- has its foot at the interval's end node, where
977/// [`crate::blend::chain::declared_wall_probe`] cannot read (the stencil
978/// cannot follow a foot out of its own interval). It is read on a JUNCTION
979/// stencil instead: the neighbouring interval's last half and this one's
980/// first half (or this one's last half and the next one's first), the same
981/// exact centres meeting at the shared station, with the probe at its
982/// middle node. Two intervals of unequal native width (local insertions)
983/// give the junction unequal node spacing, so it is then read by the
984/// native-abscissa reader (`chain::declared_wall_probe_at_why`) on the
985/// edge parameters the centres were solved at; of equal width, by the
986/// ordinary reader. With no neighbour it is read as before.
987/// The wall reading of an EXACT extrusion's rows, read directly: the wall is
988/// the ball's sweep along the centre line, so every point of the shipped
989/// surface stands exactly the radius off that line, and the reading is how
990/// far it does not. One entry per interval overlapping `window`, probes at
991/// the interval's quarters inside the window and at the window's ends, over
992/// the section at k/8 (the ball-resolve reader's sections). Never for fitted
993/// rows: those are read against the exact centres between their stations
994/// ([`open_wall_readings_why`]).
995pub(in crate::blend) fn exact_extrusion_wall_readings(
996    rows: &FittedRows,
997    parameters: &[f64],
998    window: [f64; 2],
999    radius: f64,
1000) -> Vec<Result<f64, String>> {
1001    let intervals = parameters.len().saturating_sub(1);
1002    let overlapped: Vec<usize> =
1003        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1004    let line = match (rows.exact_extrusion, rows.center.as_ref()) {
1005        (true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
1006        (false, _) => Err("the rows are not an exact extrusion".to_string()),
1007        (true, None) => Err("the exact rows carry no centre line".to_string()),
1008    };
1009    let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
1010        Ok(line) => line,
1011        Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
1012    };
1013    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1014        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1015    };
1016    overlapped
1017        .into_iter()
1018        .map(|interval| -> Result<f64, String> {
1019            let (from, to) = (parameters[interval], parameters[interval + 1]);
1020            let inside = |u: f64| u >= window[0] && u <= window[1];
1021            let mut probes: Vec<f64> =
1022                (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
1023            probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
1024            if probes.is_empty() {
1025                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1026            }
1027            let mut worst = 0.0_f64;
1028            for u in probes {
1029                for k in 1..OPEN_WALL_SECTIONS {
1030                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1031                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1032                    let offset = point.sub(anchor);
1033                    let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
1034                    let reading = (standoff - radius).abs();
1035                    if !reading.is_finite() {
1036                        return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
1037                    }
1038                    worst = worst.max(reading);
1039                }
1040            }
1041            Ok(worst)
1042        })
1043        .collect()
1044}
1045
1046/// The wall reading of an EXACT REVOLUTION's rows
1047/// (`support::exact_revolution_rows`), read directly: the wall is the ball's
1048/// sweep along the centre CIRCLE, so every point of the shipped surface
1049/// stands exactly the radius off that circle, and the reading is how far it
1050/// does not. The distance to the circle is closed-form in the point's
1051/// meridian half-plane: `hypot(ρ − R_c, h − h_c)` for the point's radial
1052/// distance ρ and axial height h about the rows' axis. Same probes as
1053/// [`exact_extrusion_wall_readings`]: one entry per interval overlapping
1054/// `window`, the interval's quarters inside the window and the window's
1055/// ends, over the section at k/8.
1056pub(in crate::blend) fn exact_revolution_wall_readings(
1057    rows: &FittedRows,
1058    parameters: &[f64],
1059    window: [f64; 2],
1060    radius: f64,
1061) -> Vec<Result<f64, String>> {
1062    let intervals = parameters.len().saturating_sub(1);
1063    let overlapped: Vec<usize> =
1064        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1065    let circle = match (rows.revolution_axis, rows.center.as_ref()) {
1066        (Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
1067        (None, _) => Err("the rows are not an exact revolution".to_string()),
1068        (Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
1069    };
1070    let (origin, axis, centre_radius, centre_height) = match circle {
1071        Ok((origin, axis, point)) => {
1072            let offset = point.sub(origin);
1073            let height = offset.dot(axis);
1074            (origin, axis, offset.sub(axis.scale(height)).length(), height)
1075        }
1076        Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
1077    };
1078    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1079        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1080    };
1081    overlapped
1082        .into_iter()
1083        .map(|interval| -> Result<f64, String> {
1084            let (from, to) = (parameters[interval], parameters[interval + 1]);
1085            let inside = |u: f64| u >= window[0] && u <= window[1];
1086            let mut probes: Vec<f64> =
1087                (1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
1088            probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
1089            if probes.is_empty() {
1090                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1091            }
1092            let mut worst = 0.0_f64;
1093            for u in probes {
1094                for k in 1..OPEN_WALL_SECTIONS {
1095                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1096                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1097                    let offset = point.sub(origin);
1098                    let height = offset.dot(axis);
1099                    let radial = offset.sub(axis.scale(height)).length();
1100                    let standoff = (radial - centre_radius).hypot(height - centre_height);
1101                    let reading = (standoff - radius).abs();
1102                    if !reading.is_finite() {
1103                        return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
1104                    }
1105                    worst = worst.max(reading);
1106                }
1107            }
1108            Ok(worst)
1109        })
1110        .collect()
1111}
1112
1113pub(in crate::blend) fn open_wall_readings_why(
1114    rows: &FittedRows,
1115    parameters: &[f64],
1116    stations: &[Station],
1117    window: [f64; 2],
1118    dense: &OpenDenseCentres,
1119    radius: f64,
1120) -> Vec<Result<f64, String>> {
1121    let intervals = parameters.len().saturating_sub(1);
1122    let overlapped: Vec<usize> =
1123        (0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
1124    let Ok([v_low, v_high]) = rows.surface.domain_v() else {
1125        return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
1126    };
1127    // The exact centres of one interval at j/32, its stations at both ends.
1128    let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
1129        let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
1130        if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
1131            return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
1132        }
1133        let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
1134        centres.push(stations[interval].center);
1135        for (j, centre) in inner.iter().enumerate() {
1136            centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
1137        }
1138        centres.push(stations[interval + 1].center);
1139        Ok(centres)
1140    };
1141    let half = OPEN_WALL_CENTRES / 2;
1142    // The junction stencil across the station between `left` and `left + 1`:
1143    // left's nodes half..=32, then right's nodes 1..=half; the station is node
1144    // `half`. With each node's NATIVE abscissa (the edge parameter its exact
1145    // centre was solved at, `OpenDenseCentres::station_ts`), and `None` when
1146    // the two intervals' native widths are bitwise equal: the nodes are then
1147    // uniform and the ordinary reader reads them, bit for bit.
1148    let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
1149        if dense.station_ts.len() != parameters.len() {
1150            return Err(format!("interval {left}: junction without the native station parameters"));
1151        }
1152        junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
1153    };
1154    overlapped
1155        .into_iter()
1156        .map(|interval| -> Result<f64, String> {
1157            let centres = nodes_of(interval)?;
1158            let (from, to) = (parameters[interval], parameters[interval + 1]);
1159            let inside = |u: f64| u >= window[0] && u <= window[1];
1160            // (u, stencil node, the stencil: None = this interval's own;
1161            // a junction's nodes with its native abscissae when unequal).
1162            let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
1163                .step_by(2)
1164                .map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
1165                .filter(|(u, _)| inside(*u))
1166                .map(|(u, step)| (u, step, None))
1167                .collect();
1168            for end in window {
1169                if end > from && end < to {
1170                    let fraction = (end - from) / (to - from);
1171                    let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
1172                    #[cfg(not(test))]
1173                    let junction_off = false;
1174                    if junction_off {
1175                        probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1176                    } else if nearest < 2 && interval > 0 {
1177                        probes.push((end, half, Some(junction(interval - 1)?)));
1178                    } else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
1179                        probes.push((end, half, Some(junction(interval)?)));
1180                    } else {
1181                        probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
1182                    }
1183                }
1184            }
1185            if probes.is_empty() {
1186                return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
1187            }
1188            let mut declared = 0.0_f64;
1189            for (u, step, stencil) in probes {
1190                let (nodes, abscissae, what) = match &stencil {
1191                    Some((nodes, None)) => (nodes, None, "junction"),
1192                    Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
1193                    None => (&centres, None, "interval"),
1194                };
1195                for k in 1..OPEN_WALL_SECTIONS {
1196                    let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
1197                    let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
1198                    // The interval's own stencil reads from the probe's node,
1199                    // then (declined) from where its fraction reaches along the
1200                    // centre path (a stationary section); a junction keeps its
1201                    // own dispatch.
1202                    let read = if stencil.is_none() {
1203                        crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
1204                    } else {
1205                        read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
1206                    };
1207                    let (reading, uncertainty) = read.map_err(|why| {
1208                        format!(
1209                            "interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
1210                             (u {u:.17}): {why}"
1211                        )
1212                    })?;
1213                    declared = declared.max(reading + uncertainty);
1214                }
1215            }
1216            if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
1217        })
1218        .collect()
1219}
1220
1221
1222/// A fresh id allocator seeded past the vertex, edge, face, loop and coedge IDs.
1223/// Solid and shell IDs are outside this allocation domain. Shared by
1224/// a whole group of stripes so their vertices, edges and coedges cannot
1225/// collide.
1226pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
1227    let mut next_id = solid
1228        .vertices
1229        .iter()
1230        .map(|vertex| vertex.id)
1231        .chain(solid.edges.iter().map(|edge| edge.id))
1232        .chain(
1233            solid
1234                .shells
1235                .iter()
1236                .flat_map(|shell| &shell.faces)
1237                .flat_map(|face| {
1238                    face.loops
1239                        .iter()
1240                        .map(|loop_record| loop_record.id)
1241                        .chain(face.loops.iter().flat_map(|loop_record| {
1242                            loop_record.coedges.iter().map(|coedge| coedge.id)
1243                        }))
1244                        .chain(std::iter::once(face.id))
1245                }),
1246        )
1247        .max()
1248        .unwrap_or(0)
1249        + 1;
1250    move || {
1251        let id = next_id;
1252        next_id += 1;
1253        id
1254    }
1255}
1256
1257
1258/// May a declined support refit fall back on the march's own trim pcurve?
1259/// Only when that trim, read independently as shipped
1260/// (`read_shipped_pcurve`: miss, rail standoff, samples, worst), meets the
1261/// floor ON its rail's branch: its miss PLUS the rail's standoff from the
1262/// carrier at the trim's foot within `floor`. A trim on another branch of the
1263/// carrier tracks its own foot perfectly (miss 0) while that foot stands far
1264/// off the rail, so a miss alone would ship it; an unreadable trim never
1265/// stands.
1266pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
1267    matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
1268}
1269
1270/// Slice 1b of the surgery lane (`exact_trim_on`): the support trim of a
1271/// straight rail on a PLANE, exact. The feet of the rail's two ends on the
1272/// plane (the kernel's closed-form projection) joined by a degree-1 pcurve
1273/// on the rail's own parameter range, so the surgery reads it at the rail's
1274/// parameter as it reads every trim; read once as shipped
1275/// (`read_shipped_pcurve`) and kept only when it stands within the pcurve
1276/// floor by the march trim's own rule (`march_trim_stands`: miss plus rail
1277/// standoff). `None` hands the trim to the refit road, as a march trim that
1278/// does not stand would be.
1279fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
1280    if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
1281        return Ok(None);
1282    }
1283    let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
1284    let mut control = Vec::with_capacity(2);
1285    for t in [d0, d1] {
1286        let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
1287        let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
1288        control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
1289    }
1290    let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
1291    feet_timing.stop();
1292    let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1293    let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
1294        Some(read) => {
1295            crate::blend::record_count("count.read.affine_trims", 1);
1296            Ok(read)
1297        }
1298        None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
1299    };
1300    read_timing.stop();
1301    Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
1302}
1303
1304/// [`read_shipped_pcurve`](crate::blend::track_fit::read_shipped_pcurve)'s
1305/// `(miss, standoff, samples, worst)` for a degree-1 `pcurve` on an AFFINE
1306/// plane against an affine rail, read at `d0` and `d1` only: the pcurve's
1307/// image, the rail and the rail's foot on the plane are all affine in the
1308/// rail parameter, so the miss (image to foot) and the standoff (rail to foot)
1309/// are norms of affine functions, convex, and largest at an end. `None` when
1310/// any of the three is not affine, or an end will not evaluate.
1311fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
1312    if !affine_curve(rail) || !affine_curve(pcurve) {
1313        return None;
1314    }
1315    let (origin, normal) = affine_plane(surface)?;
1316    let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
1317    for t in [d0, d1] {
1318        let point = rail.evaluate(t).ok()?;
1319        let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
1320        let uv = pcurve.evaluate(t).ok()?;
1321        let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
1322        let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
1323        if !(residual.is_finite() && off.is_finite()) {
1324            return None;
1325        }
1326        if residual > miss {
1327            (miss, worst) = (residual, t);
1328        }
1329        standoff = standoff.max(off);
1330    }
1331    Some((miss, standoff, 2, worst))
1332}
1333
1334/// Sew ONE stripe into `result`.
1335///
1336/// `solid` is the ORIGINAL topology every stripe was marched against (the
1337/// row-coincidence detection reads it); `result` is the shared evolving
1338/// solid, and `take_id` the shared id allocator — a group of stripes meeting
1339/// at a corner must create their rim vertices and cross-section arcs from one
1340/// counter, and must see each other's work.
1341///
1342/// Each end is either free (terminate on the face across the corner) or a
1343/// corner stop; see [`EndPlan`].  Pruning orphaned vertices is the caller's
1344/// job, once, after every stripe of the group is in.
1345pub(in crate::blend) fn build_open_surgery(
1346    solid: &BrepSolid,
1347    result: &mut BrepSolid,
1348    take_id: &mut dyn FnMut() -> u64,
1349    edge: &EdgeRecord,
1350    first: &BlendMate,
1351    second: &BlendMate,
1352    rows: FittedRows,
1353    ends: [EndPlan; 2],
1354    name: Option<&str>,
1355) -> Result<SewnStripe, KernelRefusal> {
1356    build_open_surgery_with_tail(solid, result, take_id, edge, first, second, rows, ends, name, [None, None])
1357}
1358
1359/// [`build_open_surgery`] for a stripe whose finish end rolled onto tangent
1360/// neighbours (`crate::blend::rollover::stopped_end_tail`, switch, default
1361/// off): after the pristine sewing, each rolled side's rail is split at its
1362/// crossing vertex, the part past it and the cap's leg move from the mate's
1363/// loop into the neighbour's — where they replace the run of the crossed
1364/// tangent edge from the crossing to the sharp vertex, which is deleted —
1365/// and the part past it gets its pcurve on the neighbour. The mate keeps the
1366/// rail up to the crossing and the rest of the tangent edge. With no rolled
1367/// side the sewing is exactly [`build_open_surgery`]'s.
1368#[allow(clippy::too_many_arguments)]
1369pub(in crate::blend) fn build_open_surgery_with_tail(
1370    solid: &BrepSolid,
1371    result: &mut BrepSolid,
1372    take_id: &mut dyn FnMut() -> u64,
1373    edge: &EdgeRecord,
1374    first: &BlendMate,
1375    second: &BlendMate,
1376    rows: FittedRows,
1377    ends: [EndPlan; 2],
1378    name: Option<&str>,
1379    tail: [Option<crate::blend::rollover::TailSplice<'_>>; 2],
1380) -> Result<SewnStripe, KernelRefusal> {
1381    let [start_end, finish_end] = ends;
1382
1383    // Trim the support rows to the crossing window.
1384    // A POLE end stops EXACTLY on the row's own end (the march puts the
1385    // pole's station there and overshoots no further), so there is nothing to
1386    // split off on that side.  Only equality: a stop PAST the row's end — a
1387    // free end's crossing the rows never reached — still fails the split and
1388    // refuses, rather than building a rail out to wherever the rows stop.
1389    let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
1390        let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
1391        let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
1392        if b == high {
1393            return Ok(tail);
1394        }
1395        let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
1396        Ok(middle)
1397    };
1398    let kind = |plan: &EndPlan| match plan {
1399        EndPlan::Free(_) => "free",
1400        EndPlan::Corner(_) => "corner",
1401        EndPlan::Miter(_) => "miter",
1402        EndPlan::Cap(_) => "cap",
1403    };
1404    let describe_trim = |error: KernelRefusal| {
1405        error.with_message(|error| {
1406            format!(
1407                "{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
1408                 at cr {:.6}/cs {:.6})",
1409                edge.id,
1410                kind(&start_end),
1411                start_end.cr_parameter(),
1412                start_end.cs_parameter(),
1413                kind(&finish_end),
1414                finish_end.cr_parameter(),
1415                finish_end.cs_parameter()
1416            )
1417        })
1418    };
1419    // A rail whose two stops are one point within `consumed_band` has no strip
1420    // to trim: the blend takes that mate down to a single point along this
1421    // edge.  That is a FULL-WIDTH corner — the shared face of a miter whose
1422    // radius is the side face's width, every face of a star at full width, the
1423    // middle edge of a channel whose two corner setbacks meet.  No rail edge is
1424    // built; the mate loses the blended edge instead of having it replaced, and
1425    // the rail's two rim vertices are one vertex, which the orchestrator
1426    // identifies (`SewnStripe::identified`) once every stripe is in.  A stripe
1427    // free at BOTH ends has no corner to collapse onto, so it refuses.
1428    let band = consumed_band(solid);
1429    let exact_trim = crate::blend::exact_trim_on();
1430    let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
1431        let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
1432        Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
1433            && row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
1434    };
1435    let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
1436    let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
1437    if cr_collapsed || cs_collapsed {
1438        let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
1439            Some("neither end is a corner")
1440        } else if first.face.id == second.face.id {
1441            Some("both rails lie on one face")
1442        } else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
1443            Some("a capped end has no corner to share the point with")
1444        } else {
1445            None
1446        };
1447        if let Some(reason) = unsupported {
1448            return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
1449                "{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
1450                edge.id
1451            )));
1452        }
1453    }
1454    // The support pcurve the rows carry interpolates the stations' (u, v) and
1455    // is read nowhere between them: on the 20-degree crossing's notched open
1456    // exit arc it stood 2.98e-5 off the rail it trims (q1 = 9cd8c3163 + the
1457    // section-fit fix, read against the exact cylinders). Inside the crossing
1458    // window — on the face, never on a carrier's extension — it is refitted
1459    // to the trimmed rail's own projected track at the pcurve floor and read
1460    // again as shipped (`refit_closed_support_pcurve`, the closed edge's).
1461    // A rail that overruns a PLANE carrier's chart (the rib-base fin top's
1462    // rails run 0.303 past it) is on the plane's extension, which the chart
1463    // clamps: there the refit and both reads would measure the clamp, and the
1464    // trim is instead rebuilt from its edge on the widened chart and verified
1465    // at the floor by `fit_planar_charts_to_trims`, which every lane that
1466    // sews these stripes runs.
1467    let trim_rail = |collapsed: bool,
1468                     row: &NurbsCurve,
1469                     pcurve: &NurbsCurve,
1470                     surface: &NurbsSurface,
1471                     a: f64,
1472                     b: f64|
1473     -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1474        if collapsed {
1475            return Ok(None);
1476        }
1477        let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
1478        let rail = trim_row(row, a, b).map_err(describe_trim)?;
1479        let interpolated = trim_row(pcurve, a, b)?;
1480        rows_timing.stop();
1481        {
1482            let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
1483            let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1484            let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
1485            overrun_timing.stop();
1486            if overrun.is_some_and(|excursion| excursion > band) {
1487                return Ok(Some((rail, interpolated)));
1488            }
1489        }
1490        let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
1491        // Slice 1b (`exact_trim_on`): an exact extrusion row's rail on a
1492        // plane ships the exact line in uv, read once; the refit road below
1493        // is for what it declines.
1494        if exact_trim && rows.exact_extrusion {
1495            if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
1496                crate::blend::record_count("count.surgery.exact_trim", 1);
1497                return Ok(Some((rail, line)));
1498            }
1499        }
1500        // The exact REVOLUTION rows' own trims are exact by construction
1501        // (the carrier's uv line, the plane's affine image of the rail):
1502        // read once as shipped by the march trim's rule (`march_trim_stands`)
1503        // and kept; whatever does not stand takes the refit road below.
1504        if rows.revolution_axis.is_some() {
1505            // Lane N row 4: the pair bounded in closed form stands without a
1506            // sample (`exact_revolution_read_bound` on the UNCUT row and
1507            // pcurve, whose restrictions to [a, b] these are: miss + standoff
1508            // within three bounds).
1509            if exact_revolution_reads_on() {
1510                let cert_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.cert_ms");
1511                let bound = exact_revolution_read_bound(row, surface, pcurve);
1512                cert_timing.stop();
1513                if bound.is_some_and(|bound| 3.0 * bound <= crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1514                    crate::blend::record_count("count.surgery.exact_revolution_trim_certified", 1);
1515                    return Ok(Some((rail, interpolated)));
1516                }
1517            }
1518            let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
1519            let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
1520            read_timing.stop();
1521            if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
1522                crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
1523                return Ok(Some((rail, interpolated)));
1524            }
1525            crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
1526        }
1527        // The march's OWN trim pcurve, read independently at the rail's own
1528        // parameter against the same floor (`read_shipped_pcurve`): what a
1529        // declined refit may fall back to, and the standoff a refit on the
1530        // rail's own branch must match.
1531        // The feet of each pcurve against the rail are sought once and
1532        // shared between the reads below (`FootMemo`): the march trim's for
1533        // its own read and the branch comparison, the refit's for the
1534        // refit's own read, the branch comparison and the absolute bound.
1535        let mut march_feet = crate::blend::track_fit::FootMemo::default();
1536        let mut refit_feet = crate::blend::track_fit::FootMemo::default();
1537        let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
1538        let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
1539        march_read_timing.stop();
1540        let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
1541        let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
1542            // Kept ONLY when the march's trim stands on its own read
1543            // (`march_trim_stands`); otherwise the refusal stands.
1544            let stands = march_trim_stands(&march_read, floor);
1545            if stands {
1546                Ok(Some((rail.clone(), interpolated.clone())))
1547            } else {
1548                Err(why)
1549            }
1550        };
1551        let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
1552        let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
1553        refit_timing.stop();
1554        let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
1555        match refit {
1556            Ok((fitted, refit_read)) => {
1557                // A refit tracks ITS OWN projected track; a track that took
1558                // another branch of the carrier tracks itself perfectly while
1559                // its foot stands far off the rail (the stationary-start
1560                // prism's coedge read 6.0 off its edge). Its foot standoff
1561                // must be the rail's own standoff on the march trim's branch.
1562                // BOTH reads are required: an unreadable refit read declines
1563                // the refit (the march trim then stands only on its own read
1564                // within the floor), and an unreadable march read cannot
1565                // establish that the refit is on the rail's branch, so it
1566                // refuses. Never shipped on an unknown.
1567                // The two are compared POINTWISE, at the same rail
1568                // parameters (`standoff_excess`), never as two maxima read at
1569                // each curve's own knots.
1570                // The refit's read is the shipped curve's own, handed back by
1571                // the refit (the same `read_shipped_pcurve` on the same
1572                // inputs, once).
1573                let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
1574                    (_, Err(error)) => return declined(error),
1575                    (Err(error), Ok(_)) => {
1576                        return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
1577                            "blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
1578                             trim is unreadable ({})",
1579                            error.message
1580                        )));
1581                    }
1582                    (Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
1583                };
1584                let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
1585                let excess = crate::blend::track_fit::standoff_excess_memo(
1586                    surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
1587                );
1588                excess_timing.stop();
1589                let excess = match excess {
1590                    Ok(excess) => excess,
1591                    Err(error) => return declined(error),
1592                };
1593                if !(excess <= floor) {
1594                    return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
1595                        "blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
1596                         at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
1597                         another branch of the carrier"
1598                    )));
1599                }
1600                // ABSOLUTE: the comparison above is relative to the march
1601                // trim, so a refit that follows an off-branch march trim
1602                // passes it. The refit's foot must also stand within the floor
1603                // of the rail's own deviation from its carrier, read by the
1604                // nearest-point projector at the same parameters
1605                // (`standoff_over_nearest`) -- the rail's approximation the
1606                // lane reports, never more.
1607                let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
1608                let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
1609                nearest_timing.stop();
1610                let absolute = match absolute {
1611                    Ok(absolute) => absolute,
1612                    Err(error) => return declined(error),
1613                };
1614                if !(absolute <= floor) {
1615                    return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
1616                        "blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
1617                         off its carrier: its foot is not the rail's nearest point"
1618                    )));
1619                }
1620                Ok(Some((rail, fitted)))
1621            }
1622            Err(error) => declined(error),
1623        }
1624    };
1625    let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
1626    let cr_rail = trim_rail(
1627        cr_collapsed,
1628        &rows.cr,
1629        &rows.cr_pcurve,
1630        &first.face.surface,
1631        start_end.cr_parameter(),
1632        finish_end.cr_parameter(),
1633    )?;
1634    let cs_rail = trim_rail(
1635        cs_collapsed,
1636        &rows.cs,
1637        &rows.cs_pcurve,
1638        &second.face.surface,
1639        start_end.cs_parameter(),
1640        finish_end.cs_parameter(),
1641    )?;
1642    trim_timing.stop();
1643    // A collapsed rail's domain is its two stops, which are one point.
1644    let cr_domain = match &cr_rail {
1645        Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
1646        None => [start_end.cr_parameter(), finish_end.cr_parameter()],
1647    };
1648    let cs_domain = match &cs_rail {
1649        Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
1650        None => [start_end.cs_parameter(), finish_end.cs_parameter()],
1651    };
1652    // A rail's end points, read off the trimmed rail or, collapsed, the row.
1653    let cr_at = |parameter: f64| match &cr_rail {
1654        Some((cr, _)) => cr.evaluate(parameter),
1655        None => rows.cr.evaluate(parameter),
1656    };
1657    let cs_at = |parameter: f64| match &cs_rail {
1658        Some((cs, _)) => cs.evaluate(parameter),
1659        None => rows.cs.evaluate(parameter),
1660    };
1661    let old_start = edge.start_vertex_id;
1662    let old_finish = edge.end_vertex_id;
1663
1664    // Classify each of the four support crossings.  A crossing that lands at
1665    // the FAR endpoint of the boundary edge it meets (the end away from the
1666    // corner) means a prior fillet already consumed that whole boundary: the
1667    // new blend reuses the existing vertex there and the boundary edge is
1668    // deleted (its role is taken over by the new transverse curve on the
1669    // prior blend face).  An interior crossing is the pristine case — a fresh
1670    // vertex with the boundary trimmed to it.
1671    //
1672    // Consumed means within `consumed_band` — the slack `check_support_extent`
1673    // refuses past, so a rail between the two is not possible.
1674    let classify =
1675        |boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
1676            let boundary = result
1677                .edges
1678                .iter()
1679                .find(|candidate| candidate.id == boundary_id)
1680                .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
1681            let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
1682            let far_vertex = if boundary.start_vertex_id == corner {
1683                Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
1684            } else if boundary.end_vertex_id == corner {
1685                Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
1686            } else {
1687                None
1688            };
1689            if let Some((far_id, far_t, near_t)) = far_vertex {
1690                // A consumed boundary is a COINCIDENCE — a prior fillet's rim
1691                // vertex is exactly where this rail crosses — and the operands
1692                // were healed before the march, so it holds to solver
1693                // precision.  It is not "near the far end": a fillet whose
1694                // radius is 1e-3 short of the face's width crosses 1e-3 from
1695                // the far vertex and must keep that sliver, or the face is
1696                // snapped a full 1e-3 out of true (the oversized-radius limit
1697                // cases and issue 1177 measure exactly that).  Measured in
1698                // model units, never in the boundary's parameter — a unit
1699                // parameter on a 20-long edge would call 2e-3 a coincidence.
1700                let far_point = result
1701                    .vertices
1702                    .iter()
1703                    .find(|candidate| candidate.id == far_id)
1704                    .map(|candidate| candidate.point)
1705                    .ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
1706                let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
1707                let _ = span;
1708                let past_far = crossing.sub(far_point).length();
1709                if past_far <= band {
1710                    // Sanity: the reused vertex must exist.
1711                    if !result
1712                        .vertices
1713                        .iter()
1714                        .any(|candidate| candidate.id == far_id)
1715                    {
1716                        return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
1717                    }
1718                    return Ok(RimResolution::Consumed(far_id));
1719                }
1720                // A crossing BEYOND the far vertex is a rail that runs off
1721                // its face: the blend is wider than the face by more than the
1722                // band a snap may close, and trimming the boundary there would
1723                // extend it into air.  `check_support_extent` refuses this
1724                // before the march on every face the exact tool understands;
1725                // this is the refusal for the rest.  It is not the whole
1726                // answer: a crossing the solve CLAMPS to the far vertex reads
1727                // as consumed here, and `detect_row_coincidence` is what sees
1728                // that row leave the face.  Measured on the census: it fires on
1729                // a wall wider than its closed chain's arc (0.5 past) and on the
1730                // r = 10.001 pinch past an earlier round (2e-3 past), both of
1731                // which refused later in the surgery before.
1732                if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
1733                    return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
1734                        "{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
1735                         past that edge's far vertex, and a rail within {band:.3e} of it is the \
1736                         most this surgery snaps onto the edge"
1737                    )));
1738                }
1739            }
1740            Ok(RimResolution::Fresh)
1741        };
1742    // A CORNER end has no boundary to classify: the ball's tangency vertex is
1743    // the rim, and the boundary edge that used to reach the sharp corner is
1744    // the neighbouring stripe's own blended edge, which that stripe replaces.
1745    let resolve_side = |plan: &EndPlan,
1746                        side_first: bool,
1747                        corner: u64|
1748     -> Result<RimResolution, KernelRefusal> {
1749        if let Some(vertex) = plan.planned_rim(side_first) {
1750            return Ok(RimResolution::Corner(vertex));
1751        }
1752        match plan {
1753            EndPlan::Free(free) => classify(
1754                if side_first {
1755                    free.first_edge_id
1756                } else {
1757                    free.second_edge_id
1758                },
1759                if side_first {
1760                    free.first_edge_parameter
1761                } else {
1762                    free.second_edge_parameter
1763                },
1764                corner,
1765            ),
1766            EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
1767                Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
1768            }
1769        }
1770    };
1771    let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
1772    let start_first = resolve_side(&start_end, true, old_start)?;
1773    let start_second = resolve_side(&start_end, false, old_start)?;
1774    let finish_first = resolve_side(&finish_end, true, old_finish)?;
1775    let finish_second = resolve_side(&finish_end, false, old_finish)?;
1776    crossings_timing.stop();
1777    let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
1778
1779    // Manifold pairing: the blend's use of the first support row must oppose
1780    // F1's use of the blended edge (see the blend-loop construction below).
1781    let first_use_forward = first.coedge.forward;
1782    let blend_cr_forward = !first_use_forward;
1783
1784    // Curve-level coincidence (OCCT PR #1449 lesson 3): when BOTH of a mate's
1785    // crossings are endpoint-consumed AND the trimmed support row retraces the
1786    // existing edge joining the two far vertices, that mate face is FULLY
1787    // consumed — the surgery sews the blend face straight onto the existing
1788    // edge and drops the zero-width face, instead of creating a coincident
1789    // fresh support edge that would leave a sliver strip (the r = face-width
1790    // class).  Each side is detected independently; any ambiguity keeps the
1791    // pristine path (fail-safe).  `row_traversed_from_start`: the blend loop
1792    // walks cr in station order iff `blend_cr_forward`, and cs in the
1793    // OPPOSITE order (see the two loop branches below).
1794    // A stripe with a CORNER end has no boundary edges at that end to be
1795    // coincident with, so the detection only runs on stripes that are free at
1796    // both ends.  (Fail-safe: not detecting a coincidence keeps the pristine
1797    // path, which is what a corner stop wants anyway.)
1798    let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
1799        (Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
1800            detect_row_coincidence(
1801                solid,
1802                first,
1803                edge.id,
1804                start_free.first_edge_id,
1805                finish_free.first_edge_id,
1806                start_first.is_consumed(),
1807                start_first.existing().unwrap_or(0),
1808                finish_first.is_consumed(),
1809                finish_first.existing().unwrap_or(0),
1810                cr,
1811                blend_cr_forward,
1812                band,
1813            )?,
1814            detect_row_coincidence(
1815                solid,
1816                second,
1817                edge.id,
1818                start_free.second_edge_id,
1819                finish_free.second_edge_id,
1820                start_second.is_consumed(),
1821                start_second.existing().unwrap_or(0),
1822                finish_second.is_consumed(),
1823                finish_second.existing().unwrap_or(0),
1824                cs,
1825                !blend_cr_forward,
1826                band,
1827            )?,
1828        ),
1829        _ => (None, None),
1830    };
1831    // One face hosting both sides (the blended edge used twice by one face)
1832    // cannot be dropped for one side while the other still splices into it —
1833    // ambiguous, keep the pristine path for both.
1834    let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
1835        if first.face.id == second.face.id {
1836            (None, None)
1837        } else {
1838            (sew_first, sew_second)
1839        };
1840
1841    // Resolve the four rim vertices: reuse the existing vertex for a consumed
1842    // side, else create a fresh vertex at the support-row endpoint.
1843    let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
1844        match rim.existing() {
1845            Some(id) => Ok(id),
1846            None => {
1847                let id = take_id();
1848                result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
1849                Ok(id)
1850            }
1851        }
1852    };
1853    let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
1854    let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
1855    let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
1856    let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
1857    // How far the consumed rims and sewn rows moved a rail onto what already
1858    // existed — what `check_snap_closure` measures the body against.
1859    let mut snap = [&sew_first, &sew_second]
1860        .into_iter()
1861        .flatten()
1862        .fold(0.0f64, |worst, sew| worst.max(sew.deviation));
1863    for (rim, vertex, row_end) in [
1864        (&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
1865        (&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
1866        (&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
1867        (&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
1868    ] {
1869        if !rim.is_consumed() {
1870            continue;
1871        }
1872        let point = result
1873            .vertices
1874            .iter()
1875            .find(|candidate| candidate.id == vertex)
1876            .ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
1877            .point;
1878        snap = snap.max(row_end.sub(point).length());
1879    }
1880
1881    // Support edges: a sewn side reuses the EXISTING coincident edge (no
1882    // fresh edge — OCCT's `SetExistingEdge` move); a pristine side gets the
1883    // fitted row as a fresh edge.
1884    // A collapsed side builds no edge at all.
1885    let cr_edge_id = match (&sew_first, &cr_rail) {
1886        (Some(sew), _) => Some(sew.edge_id),
1887        (None, None) => None,
1888        (None, Some((cr, _))) => {
1889            let id = take_id();
1890            result.edges.push(EdgeRecord {
1891                id,
1892                curve: cr.clone(),
1893                t0: cr_domain[0],
1894                t1: cr_domain[1],
1895                start_vertex_id: w1a,
1896                end_vertex_id: w1b,
1897                degenerate: false,
1898                name: None,
1899            });
1900            Some(id)
1901        }
1902    };
1903    let cs_edge_id = match (&sew_second, &cs_rail) {
1904        (Some(sew), _) => Some(sew.edge_id),
1905        (None, None) => None,
1906        (None, Some((cs, _))) => {
1907            let id = take_id();
1908            result.edges.push(EdgeRecord {
1909                id,
1910                curve: cs.clone(),
1911                t0: cs_domain[0],
1912                t1: cs_domain[1],
1913                start_vertex_id: w2a,
1914                end_vertex_id: w2b,
1915                degenerate: false,
1916                name: None,
1917            });
1918            Some(id)
1919        }
1920    };
1921    // The edge closing each end of the blend face: the §6.9 transverse curve
1922    // on the end face for a free end, or — at a corner — the end
1923    // cross-section arc the corner patch was already given, committed by the
1924    // caller and merely referenced here.
1925    let mut commit_end_edge = |plan: &EndPlan,
1926                               first_rim: u64,
1927                               second_rim: u64|
1928     -> Result<Option<u64>, KernelRefusal> {
1929        match plan {
1930            EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
1931            EndPlan::Free(free) => {
1932                let id = take_id();
1933                let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
1934                result.edges.push(EdgeRecord {
1935                    id,
1936                    curve: free.transverse_curve.clone(),
1937                    t0: domain[0],
1938                    t1: domain[1],
1939                    start_vertex_id: first_rim,
1940                    end_vertex_id: second_rim,
1941                    degenerate: false,
1942                    name: None,
1943                });
1944                Ok(Some(id))
1945            }
1946        }
1947    };
1948    let transverse_a_id = commit_end_edge(&start_end, w1a, w2a)?;
1949    let transverse_b_id = commit_end_edge(&finish_end, w1b, w2b)?;
1950
1951    // Replace the blended edge in each PRISTINE mate's loop — the blend
1952    // face's loop direction is forced by manifold pairing with F1's use of
1953    // the blended edge (`first_use_forward`, captured above).  A sewn mate is
1954    // dropped whole below; nothing to splice.  A COLLAPSED side has no rail to
1955    // put there: the mate simply loses the blended edge, and its loop closes
1956    // through the rim vertices the orchestrator identifies.
1957    for (mate, rail, sewn) in [
1958        (first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
1959        (second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
1960    ] {
1961        if sewn {
1962            continue;
1963        }
1964        let face = result
1965            .shells
1966            .iter_mut()
1967            .flat_map(|shell| &mut shell.faces)
1968            .find(|face| face.id == mate.face.id)
1969            .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
1970        let loop_record = &mut face.loops[mate.loop_index];
1971        let position = loop_record
1972            .coedges
1973            .iter()
1974            .position(|coedge| coedge.edge_id == edge.id)
1975            .ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
1976        let Some((new_edge_id, (_, pcurve_forward))) = rail else {
1977            loop_record.coedges.remove(position);
1978            continue;
1979        };
1980        let old_forward = loop_record.coedges[position].forward;
1981        loop_record.coedges[position] = CoedgeRecord {
1982            id: loop_record.coedges[position].id,
1983            edge_id: new_edge_id,
1984            forward: old_forward,
1985            pcurve: if old_forward {
1986                pcurve_forward.clone()
1987            } else {
1988                pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
1989            },
1990        };
1991    }
1992
1993    // A capped end's legs: on each mate, the leg runs from the rail's rim
1994    // vertex to the sharp vertex, where the continuing edge still starts.
1995    // It goes between the rail coedge and that continuing coedge, with the
1996    // sense that walks rim -> vertex when the rail ends at the rim.
1997    for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
1998        let EndPlan::Cap(cap) = plan else {
1999            continue;
2000        };
2001        for (mate, rail_edge_id, leg, rim) in [
2002            (first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
2003            (second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
2004        ] {
2005            // Refused above: a capped end never meets a collapsed rail.
2006            let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
2007            let face = result
2008                .shells
2009                .iter_mut()
2010                .flat_map(|shell| &mut shell.faces)
2011                .find(|face| face.id == mate.face.id)
2012                .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
2013            let loop_record = &mut face.loops[mate.loop_index];
2014            let count = loop_record.coedges.len();
2015            let rail_at = loop_record
2016                .coedges
2017                .iter()
2018                .position(|coedge| coedge.edge_id == rail_edge_id)
2019                .ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
2020            // Which side of the rail coedge is this end?  The rail coedge
2021            // traverses rim-to-rim; the capped end is at its traversal END
2022            // when walking forward means station order and this is the
2023            // finish end, etc.  Decide by geometry: the neighbour whose
2024            // traversal touches the sharp vertex.
2025            let rail_forward = loop_record.coedges[rail_at].forward;
2026            let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
2027            let (insert_at, forward, pcurve) = if end_is_traversal_end {
2028                // rail ... rim -> [leg rim->vertex] -> continuing edge
2029                (rail_at + 1, true, leg.1.clone())
2030            } else {
2031                // continuing edge -> [leg vertex->rim] -> rim ... rail
2032                (rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
2033            };
2034            let _ = (count, rim);
2035            loop_record.coedges.insert(
2036                insert_at,
2037                CoedgeRecord {
2038                    id: take_id(),
2039                    edge_id: leg.0,
2040                    forward,
2041                    pcurve,
2042                },
2043            );
2044        }
2045    }
2046
2047    // Drop consumed boundary edges from the mate loop they share with the
2048    // blended edge: the fresh support coedge already begins at the reused far
2049    // vertex, so removing the fully-covered boundary keeps the loop closed.
2050    // A SEWN mate's whole loop collapses (the face is dropped below), so its
2051    // consumed boundaries are only recorded for deletion, never spliced.
2052    let mut consumed_edges: Vec<u64> = Vec::new();
2053    let start_free = start_end.free();
2054    let finish_free = finish_end.free();
2055    for (mate, sewn, sides) in [
2056        (
2057            first,
2058            sew_first.is_some(),
2059            [
2060                (&start_first, start_free.map(|free| free.first_edge_id)),
2061                (&finish_first, finish_free.map(|free| free.first_edge_id)),
2062            ],
2063        ),
2064        (
2065            second,
2066            sew_second.is_some(),
2067            [
2068                (&start_second, start_free.map(|free| free.second_edge_id)),
2069                (&finish_second, finish_free.map(|free| free.second_edge_id)),
2070            ],
2071        ),
2072    ] {
2073        for (cross, boundary_id) in sides {
2074            if !cross.is_consumed() {
2075                continue;
2076            }
2077            let Some(boundary_id) = boundary_id else {
2078                continue;
2079            };
2080            if !sewn {
2081                let face = result
2082                    .shells
2083                    .iter_mut()
2084                    .flat_map(|shell| &mut shell.faces)
2085                    .find(|face| face.id == mate.face.id)
2086                    .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
2087                face.loops[mate.loop_index]
2088                    .coedges
2089                    .retain(|coedge| coedge.edge_id != boundary_id);
2090            }
2091            if !consumed_edges.contains(&boundary_id) {
2092                consumed_edges.push(boundary_id);
2093            }
2094        }
2095    }
2096    // Lesson 7: zero-span leftovers of a collapsing loop go with their face.
2097    for sew in [&sew_first, &sew_second].into_iter().flatten() {
2098        for id in &sew.collapsed_edges {
2099            if !consumed_edges.contains(id) {
2100                consumed_edges.push(*id);
2101            }
2102        }
2103    }
2104
2105    // Locate the corner junction on each end face BEFORE trimming (the two
2106    // boundary coedges that meet at the old corner vertex, stepping over the
2107    // pole between them where the end face's apex IS that vertex) — edge ids
2108    // alone are ambiguous on two-coedge cap loops, so the meeting must be at
2109    // the corner.
2110    // (end, transverse_id, corner, first_consumed, second_consumed) -> plan.
2111    // Only FREE ends appear here: a corner stop rebuilds nothing on a third
2112    // face, because the corner patch is what closes it.
2113    let mut end_plans = Vec::with_capacity(2);
2114    for (end, transverse_id, corner, first_consumed, second_consumed) in [
2115        (
2116            start_end.free(),
2117            transverse_a_id,
2118            old_start,
2119            start_first.is_consumed(),
2120            start_second.is_consumed(),
2121        ),
2122        (
2123            finish_end.free(),
2124            transverse_b_id,
2125            old_finish,
2126            finish_first.is_consumed(),
2127            finish_second.is_consumed(),
2128        ),
2129    ] {
2130        let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
2131            continue;
2132        };
2133        let face = result
2134            .shells
2135            .iter()
2136            .flat_map(|shell| &shell.faces)
2137            .find(|face| face.id == end.end_face_id)
2138            .ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
2139        // Key on stable coedge ids (not indices) so processing one end can't
2140        // invalidate another that shares this face.
2141        let Some(located) = locate_end_corner(
2142            &result,
2143            face,
2144            corner,
2145            end.first_edge_id,
2146            end.second_edge_id,
2147        ) else {
2148            return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
2149        };
2150        // The apex the transverse curve cuts off goes with the boundaries the
2151        // blend consumes.
2152        for pole in &located.pole_edge_ids {
2153            if !consumed_edges.contains(pole) {
2154                consumed_edges.push(*pole);
2155            }
2156        }
2157        end_plans.push((
2158            end.end_face_id,
2159            transverse_id,
2160            end.transverse_end_pcurve.clone(),
2161            located,
2162            first_consumed,
2163            second_consumed,
2164        ));
2165    }
2166
2167    // Trim the boundary edges at their support crossings.  A consumed side is
2168    // deleted instead of trimmed; a CORNER side touches no boundary at all.
2169    for (rim_resolution, boundary, corner, rim) in [
2170        (
2171            &start_first,
2172            start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2173            old_start,
2174            w1a,
2175        ),
2176        (
2177            &start_second,
2178            start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2179            old_start,
2180            w2a,
2181        ),
2182        (
2183            &finish_first,
2184            finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
2185            old_finish,
2186            w1b,
2187        ),
2188        (
2189            &finish_second,
2190            finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
2191            old_finish,
2192            w2b,
2193        ),
2194    ] {
2195        if !rim_resolution.trims_boundary() {
2196            continue;
2197        }
2198        let Some((boundary_id, boundary_param)) = boundary else {
2199            continue;
2200        };
2201        trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
2202    }
2203
2204    // Rebuild each end face's corner: drop the consumed boundary coedge(s) and
2205    // splice in the transverse coedge between the (trimmed) boundaries.
2206    for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
2207        end_plans
2208    {
2209        let forward = located.x_is_first;
2210        let pcurve = if forward {
2211            transverse_end_pcurve
2212        } else {
2213            transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
2214        };
2215        let (x_consumed, y_consumed) = if located.x_is_first {
2216            (first_consumed, second_consumed)
2217        } else {
2218            (second_consumed, first_consumed)
2219        };
2220        let transverse_coedge = CoedgeRecord {
2221            id: take_id(),
2222            edge_id: transverse_id,
2223            forward,
2224            pcurve,
2225        };
2226        splice_end_corner(
2227            result,
2228            end_face,
2229            &located,
2230            transverse_coedge,
2231            x_consumed,
2232            y_consumed,
2233        )?;
2234    }
2235
2236    // Blend face: cr fwd -> TB -> cs rev -> TA rev in (t, z) space.  Both rows
2237    // share one parameterisation, so a collapsed cr reads its sense across the
2238    // strip the cs rail still spans (a stripe with BOTH rails collapsed has no
2239    // area; the orchestrator drops it with the edges that bound it).
2240    let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
2241        (Some((_, cr_pcurve)), _) => {
2242            let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
2243            (mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2244        }
2245        (None, Some(_)) => {
2246            let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
2247            (mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
2248        }
2249        (None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
2250    };
2251    let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
2252    let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
2253    let out1 = if first.face.same_sense {
2254        n1
2255    } else {
2256        n1.scale(-1.0)
2257    };
2258    let same_sense = blend_normal.dot(out1) >= 0.0;
2259    let loop_id = take_id();
2260    // Traversal senses of the blend's support coedges.  A fresh support edge
2261    // is parameterized in station order, so the sense is the loop's walking
2262    // direction (`blend_cr_forward` for cr, its opposite for cs); a SEWN
2263    // side's sense comes from the detection (the existing edge's own
2264    // parameter direction relative to that same walk — equal to the dropped
2265    // face's old sense, preserving manifold pairing with the survivor).  The
2266    // pcurves always follow the LOOP's walking direction and are unchanged.
2267    let cr_forward = sew_first
2268        .as_ref()
2269        .map_or(blend_cr_forward, |sew| sew.forward);
2270    let cs_forward = sew_second
2271        .as_ref()
2272        .map_or(!blend_cr_forward, |sew| sew.forward);
2273    // The two end slots of the blend loop.  `natural_forward` is the sense the
2274    // loop walks a transverse edge stored first-rim -> second-rim; a CORNER
2275    // arc was already committed in the walk direction (so the corner patch can
2276    // take it the other way round), so it is always used forward.
2277    // A slot is one coedge for a free end (the transverse curve) or a corner
2278    // end (the section arc), and one or two for a miter end (the seam, then
2279    // the sibling's section when the seam left the sibling first).  A corner
2280    // arc was committed in the walk direction; a miter edge records its own
2281    // sense along the walk.
2282    let mut end_slot = |plan: &EndPlan,
2283                        free_edge_id: Option<u64>,
2284                        natural_forward: bool|
2285     -> Result<Vec<CoedgeRecord>, KernelRefusal> {
2286        match plan {
2287            EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
2288                id: take_id(),
2289                edge_id: end.arc_edge_id,
2290                forward: true,
2291                pcurve: end.arc_blend_pcurve.clone(),
2292            }]),
2293            EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
2294                id: take_id(),
2295                edge_id: end.arc_edge_id,
2296                forward: true,
2297                pcurve: end.arc_blend_pcurve.clone(),
2298            }]),
2299            EndPlan::Miter(end) => end
2300                .edges
2301                .iter()
2302                .map(|(edge_id, forward, pcurve)| {
2303                    Ok(CoedgeRecord {
2304                        id: take_id(),
2305                        edge_id: *edge_id,
2306                        forward: *forward,
2307                        pcurve: pcurve.clone(),
2308                    })
2309                })
2310                .collect(),
2311            EndPlan::Free(free) => {
2312                let edge_id =
2313                    free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
2314                Ok(vec![if natural_forward {
2315                    CoedgeRecord {
2316                        id: take_id(),
2317                        edge_id,
2318                        forward: true,
2319                        pcurve: free.transverse_blend_pcurve.clone(),
2320                    }
2321                } else {
2322                    CoedgeRecord {
2323                        id: take_id(),
2324                        edge_id,
2325                        forward: false,
2326                        pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
2327                    }
2328                }])
2329            }
2330        }
2331    };
2332    let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
2333    let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
2334    // A collapsed rail has no coedge: the end slots on either side of it meet
2335    // at its one rim point.
2336    let coedges = if blend_cr_forward {
2337        let mut coedges = Vec::new();
2338        if let Some(cr_edge_id) = cr_edge_id {
2339            coedges.push(CoedgeRecord {
2340                id: take_id(),
2341                edge_id: cr_edge_id,
2342                forward: cr_forward,
2343                pcurve: crate::sweep_topology::parameter_line(
2344                    cr_domain[0],
2345                    0.0,
2346                    cr_domain[1],
2347                    0.0,
2348                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2349            });
2350        }
2351        coedges.extend(finish_slot);
2352        if let Some(cs_edge_id) = cs_edge_id {
2353            coedges.push(CoedgeRecord {
2354                id: take_id(),
2355                edge_id: cs_edge_id,
2356                forward: cs_forward,
2357                pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
2358            });
2359        }
2360        coedges.extend(start_slot);
2361        coedges
2362    } else {
2363        let mut coedges = Vec::new();
2364        if let Some(cr_edge_id) = cr_edge_id {
2365            coedges.push(CoedgeRecord {
2366                id: take_id(),
2367                edge_id: cr_edge_id,
2368                forward: cr_forward,
2369                pcurve: crate::sweep_topology::parameter_line(
2370                    cr_domain[1],
2371                    0.0,
2372                    cr_domain[0],
2373                    0.0,
2374                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2375            });
2376        }
2377        coedges.extend(start_slot);
2378        if let Some(cs_edge_id) = cs_edge_id {
2379            coedges.push(CoedgeRecord {
2380                id: take_id(),
2381                edge_id: cs_edge_id,
2382                forward: cs_forward,
2383                pcurve: crate::sweep_topology::parameter_line(
2384                    cs_domain[0],
2385                    1.0,
2386                    cs_domain[1],
2387                    1.0,
2388                ).or_refuse(KernelStage::Refine, "parameter_line")?,
2389            });
2390        }
2391        coedges.extend(finish_slot);
2392        coedges
2393    };
2394    let blend_face_id = take_id();
2395    let mut blend_face = FaceRecord {
2396        id: blend_face_id,
2397        surface: rows.surface,
2398        same_sense,
2399        loops: vec![LoopRecord {
2400            id: loop_id,
2401            coedges,
2402        }],
2403        name: name.map(|value| value.to_string()),
2404    };
2405    if rows.exact_extrusion {
2406        transpose_face(&mut blend_face)?;
2407    }
2408    let shell_index = result
2409        .shells
2410        .iter()
2411        .position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
2412        .ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
2413    // Drop the fully-consumed mate faces (lesson 3): their loops collapsed
2414    // between the sewn edge and the blend; the consumed boundaries and
2415    // lesson-7 leftovers are deleted below, the sewn edge lives on shared by
2416    // the blend face and the surviving neighbour.
2417    for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
2418        if sew.is_some() {
2419            for shell in &mut result.shells {
2420                shell.faces.retain(|face| face.id != mate.face.id);
2421            }
2422        }
2423    }
2424    result.shells[shell_index].faces.push(blend_face);
2425
2426    result.edges.retain(|candidate| candidate.id != edge.id);
2427    // Delete the boundary edges a prior fillet's corner had left behind that
2428    // this blend fully consumed.
2429    result
2430        .edges
2431        .retain(|candidate| !consumed_edges.contains(&candidate.id));
2432    // Each collapsed rail's two rims are one vertex.
2433    let mut identified = Vec::new();
2434    for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
2435        if collapsed && from != to {
2436            identified.push((from, to));
2437        }
2438    }
2439    // The stopped-end rollover's sides: the rail past the crossing and the
2440    // cap's leg chain move onto the neighbours, and the end footprint is
2441    // re-traced on every face it touches.
2442    let mut cr_edge_id = cr_edge_id;
2443    let mut cs_edge_id = cs_edge_id;
2444    if tail.iter().any(Option::is_some) {
2445        let EndPlan::Cap(cap) = &finish_end else {
2446            return Err(KernelRefusal::unsupported(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the rolled stripe's finish end is not a cap, which is the only end this lane closes on the neighbours"));
2447        };
2448        let mut footprint = StoppedEndFootprint::default();
2449        // The cap plane at the finish vertex: through the sharp vertex, normal
2450        // the edge's tangent there; the fillet lies on the -tangent side.
2451        {
2452            let sharp = result.vertices.iter().find(|vertex| vertex.id == cap.sharp_vertex).map(|vertex| vertex.point)
2453                .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the sharp vertex vanished"))?;
2454            let d = edge.curve.derivatives(edge.t1, 1).or_refuse(KernelStage::Refine, "derivatives")?;
2455            footprint.plane = (sharp, d[1].normalized().or_refuse(KernelStage::Refine, "normalized")?);
2456            footprint.sharp_vertex = cap.sharp_vertex;
2457        }
2458        for (side, splice) in tail.into_iter().enumerate() {
2459            let Some(splice) = splice else { continue };
2460            let (mate, rail_edge_id, rim, sewn) = if side == 0 {
2461                (first, cr_edge_id, w1b, sew_first.is_some())
2462            } else {
2463                (second, cs_edge_id, w2b, sew_second.is_some())
2464            };
2465            let Some(rail_edge_id) = rail_edge_id.filter(|_| !sewn) else {
2466                return Err(KernelRefusal::unsupported(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: a rolled side's rail collapsed or sewed onto an existing edge, which this lane does not split"));
2467            };
2468            let part_on_mate = roll_side_footprint(result, take_id, mate, &splice, rail_edge_id, rim, cap.sharp_vertex, &mut footprint)?;
2469            if side == 0 { cr_edge_id = Some(part_on_mate); } else { cs_edge_id = Some(part_on_mate); }
2470        }
2471        retrace_stopped_end_footprint(result, take_id, footprint)?;
2472    }
2473    Ok(SewnStripe {
2474        cr_edge_id,
2475        cs_edge_id,
2476        blend_face_id,
2477        consumed: [sew_first.is_some(), sew_second.is_some()],
2478        snap,
2479        identified,
2480    })
2481}
2482
2483/// A stopped-end rollover's footprint on the solid, gathered side by side
2484/// and re-traced once ([`retrace_stopped_end_footprint`]).
2485#[derive(Default)]
2486struct StoppedEndFootprint {
2487    /// The new boundary of the footprint, per face: (face, edge, from, to,
2488    /// pcurve from -> to on that face).
2489    pieces: Vec<(u64, u64, u64, u64, NurbsCurve)>,
2490    /// The faces whose loops change: the mates and every face a piece lies on.
2491    faces: Vec<u64>,
2492    /// The cap plane: (sharp point, unit edge tangent at the vertex). The
2493    /// fillet lies on the side opposite the tangent.
2494    plane: (Vec3, Vec3),
2495    sharp_vertex: u64,
2496}
2497
2498/// One rolled side, after the pristine sewing: the rail is split at the
2499/// crossing vertex (on the crossed tangent edge) and the tangent edge there;
2500/// the pristine splice's rail far part and first leg piece leave the mate's
2501/// loop (they belong to the neighbours), and the footprint's new boundary on
2502/// the neighbours — the far part with its pcurve on the neighbour by
2503/// projection, and the cap's leg chain — is recorded for the re-trace.
2504/// Returns the rail part left on the mate.
2505#[allow(clippy::too_many_arguments)]
2506fn roll_side_footprint(
2507    result: &mut BrepSolid,
2508    take_id: &mut dyn FnMut() -> u64,
2509    mate: &BlendMate,
2510    splice: &crate::blend::rollover::TailSplice<'_>,
2511    rail_edge_id: u64,
2512    rim_vertex: u64,
2513    sharp_vertex: u64,
2514    footprint: &mut StoppedEndFootprint,
2515) -> Result<u64, KernelRefusal> {
2516    let what = "stopped_end_rollover";
2517    let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2518    let _ = sharp_vertex;
2519    let rolled = splice.rolled;
2520    let crossing_vertex = take_id();
2521    result.vertices.push(VertexRecord { id: crossing_vertex, point: rolled.point });
2522    let rail_parts = crate::blend::rollover_surgery::split_edge_at_vertices(result, rail_edge_id, &[(crossing_vertex, rolled.point)], take_id)?;
2523    let [rail_near, rail_far] = [rail_parts[0], rail_parts[1]];
2524    let rail_far_edge = result.edges.iter().find(|edge| edge.id == rail_far).cloned()
2525        .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the rail's far part vanished"))?;
2526    if rail_far_edge.end_vertex_id != rim_vertex {
2527        return Err(not_built(format!("the rail's far part ends at vertex {} rather than the finish rim {rim_vertex}", rail_far_edge.end_vertex_id)));
2528    }
2529    // The crossed tangent edge, or the part of it the cap's leg chain left
2530    // holding the crossing (the chain may have split it already).
2531    let holds_crossing = |result: &BrepSolid, id: u64| -> Result<bool, KernelRefusal> {
2532        let Some(edge) = result.edges.iter().find(|edge| edge.id == id) else { return Ok(false) };
2533        let foot = crate::project_point_to_curve(&edge.curve, rolled.point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
2534        Ok(foot.distance <= 1e-6 && foot.u > edge.t0.min(edge.t1) && foot.u < edge.t0.max(edge.t1))
2535    };
2536    let crossed = if result.edges.iter().any(|edge| edge.id == rolled.edge_id) {
2537        rolled.edge_id
2538    } else {
2539        let candidates: Vec<u64> = result.edges.iter().map(|edge| edge.id).collect();
2540        let mut found = None;
2541        for id in candidates {
2542            if holds_crossing(result, id)? {
2543                found = Some(id);
2544                break;
2545            }
2546        }
2547        found.ok_or_else(|| not_built(format!("no part of tangent edge {} holds the crossing any more", rolled.edge_id)))?
2548    };
2549    crate::blend::rollover_surgery::split_edge_at_vertices(result, crossed, &[(crossing_vertex, rolled.point)], take_id)?;
2550    // The neighbour's pcurve of the rail's far part, by projection.
2551    let neighbour_surface = result
2552        .shells
2553        .iter()
2554        .flat_map(|shell| &shell.faces)
2555        .find(|face| face.id == rolled.neighbour_face_id)
2556        .map(|face| face.surface.clone())
2557        .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the neighbour face vanished"))?;
2558    let far_pcurve = {
2559        const SAMPLES: usize = 24;
2560        let [t0, t1] = [rail_far_edge.t0, rail_far_edge.t1];
2561        let mut uvs = Vec::with_capacity(SAMPLES + 1);
2562        let mut ts = Vec::with_capacity(SAMPLES + 1);
2563        for k in 0..=SAMPLES {
2564            let t = t0 + (t1 - t0) * k as f64 / SAMPLES as f64;
2565            let point = rail_far_edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
2566            let projection = crate::project_point_to_surface(&neighbour_surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
2567            uvs.push(Vec3::new(projection.u, projection.v, 0.0));
2568            ts.push(t);
2569        }
2570        // Clamped over the rail part's own parameter interval (the edge's).
2571        crate::fit::interpolate_curve(&uvs, 3, &ts).or_refuse(KernelStage::Refine, "interpolate_curve")?
2572    };
2573    // The pristine splice's artefacts leave the mate's loop.
2574    let leg_edge_id = splice.legs.first().map(|piece| piece.edge_id)
2575        .ok_or_else(|| not_built("a rolled side has no cap leg".to_string()))?;
2576    let mate_face = result
2577        .shells
2578        .iter_mut()
2579        .flat_map(|shell| shell.faces.iter_mut())
2580        .find(|face| face.id == mate.face.id)
2581        .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the mate face vanished"))?;
2582    mate_face.loops[mate.loop_index].coedges.retain(|coedge| coedge.edge_id != rail_far && coedge.edge_id != leg_edge_id);
2583    footprint.pieces.push((rolled.neighbour_face_id, rail_far, crossing_vertex, rim_vertex, far_pcurve));
2584    for piece in &splice.legs {
2585        footprint.pieces.push((piece.face_id, piece.edge_id, piece.from, piece.to, piece.pcurve.clone()));
2586    }
2587    for face_id in [mate.face.id, rolled.neighbour_face_id].into_iter().chain(splice.legs.iter().map(|piece| piece.face_id)) {
2588        if !footprint.faces.contains(&face_id) {
2589            footprint.faces.push(face_id);
2590        }
2591    }
2592    Ok(rail_near)
2593}
2594
2595/// Re-trace the footprint: an existing edge of an affected face whose both
2596/// ends lie on the footprint's new boundary and whose midpoint lies on the
2597/// fillet's side of the cap plane is INTERIOR to the footprint and deleted
2598/// (F10: edge 46 between the crossing and the cap leg's exit, edge 68
2599/// between its crossing and vertex 34); then every affected loop is traced
2600/// afresh through its remaining coedges and the new pieces — each vertex
2601/// of the new boundary has exactly one way on — the first closed trace
2602/// keeping the loop, every further one its own face on the same carrier
2603/// (F10: face 13's triangle beyond the cap plane).
2604fn retrace_stopped_end_footprint(
2605    result: &mut BrepSolid,
2606    take_id: &mut dyn FnMut() -> u64,
2607    footprint: StoppedEndFootprint,
2608) -> Result<(), KernelRefusal> {
2609    let what = "stopped_end_rollover";
2610    let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
2611    let (sharp_point, tangent) = footprint.plane;
2612    let mut boundary_vertices: Vec<u64> = vec![footprint.sharp_vertex];
2613    for (_, _, from, to, _) in &footprint.pieces {
2614        for vertex in [*from, *to] {
2615            if !boundary_vertices.contains(&vertex) {
2616                boundary_vertices.push(vertex);
2617            }
2618        }
2619    }
2620    let piece_edges: Vec<u64> = footprint.pieces.iter().map(|piece| piece.1).collect();
2621    // Interior edges, among those on the affected faces.
2622    let mut interior: Vec<u64> = Vec::new();
2623    for face in result.shells.iter().flat_map(|shell| &shell.faces).filter(|face| footprint.faces.contains(&face.id)) {
2624        for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
2625            let Some(edge) = result.edges.iter().find(|edge| edge.id == coedge.edge_id) else { continue };
2626            if piece_edges.contains(&edge.id) || interior.contains(&edge.id) {
2627                continue;
2628            }
2629            if !(boundary_vertices.contains(&edge.start_vertex_id) && boundary_vertices.contains(&edge.end_vertex_id)) {
2630                continue;
2631            }
2632            let mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).or_refuse(KernelStage::Refine, "evaluate")?;
2633            if mid.sub(sharp_point).dot(tangent) < 0.0 {
2634                interior.push(edge.id);
2635            }
2636        }
2637    }
2638    if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
2639        eprintln!("stopped-end rollover: footprint faces {:?}, interior edges {:?}, boundary vertices {:?}", footprint.faces, interior, boundary_vertices);
2640    }
2641    let edge_ends: std::collections::HashMap<u64, (u64, u64)> = result.edges.iter().map(|edge| (edge.id, (edge.start_vertex_id, edge.end_vertex_id))).collect();
2642    let mut new_faces: Vec<FaceRecord> = Vec::new();
2643    // Face names the result uses (the app keys faces by name; this model
2644    // already carries `_1` splits of its own): a remnant takes a free suffix.
2645    let mut taken: Vec<String> = result.shells.iter().flat_map(|shell| &shell.faces).filter_map(|face| face.name.clone()).collect();
2646    for face_id in footprint.faces.clone() {
2647        let face = result
2648            .shells
2649            .iter_mut()
2650            .flat_map(|shell| shell.faces.iter_mut())
2651            .find(|face| face.id == face_id)
2652            .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: face {face_id} vanished")))?;
2653        let pieces: Vec<&(u64, u64, u64, u64, NurbsCurve)> = footprint.pieces.iter().filter(|piece| piece.0 == face_id).collect();
2654        let mut rebuilt_loops: Vec<LoopRecord> = Vec::new();
2655        for loop_record in &face.loops {
2656            let touched = loop_record.coedges.iter().any(|coedge| interior.contains(&coedge.edge_id))
2657                || loop_record.coedges.iter().any(|coedge| {
2658                    edge_ends.get(&coedge.edge_id).is_some_and(|(a, b)| boundary_vertices.contains(a) || boundary_vertices.contains(b))
2659                });
2660            if !touched {
2661                rebuilt_loops.push(loop_record.clone());
2662                continue;
2663            }
2664            // Directed remaining coedges (direction fixed, with their place in
2665            // the old loop) and undirected pieces.
2666            let n = loop_record.coedges.len();
2667            let mut remaining: Vec<(CoedgeRecord, u64, u64, usize)> = Vec::new();
2668            for (index, coedge) in loop_record.coedges.iter().enumerate() {
2669                if interior.contains(&coedge.edge_id) {
2670                    continue;
2671                }
2672                let (start, end) = edge_ends.get(&coedge.edge_id).copied()
2673                    .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: coedge {} names a missing edge {}", coedge.id, coedge.edge_id)))?;
2674                let (from, to) = if coedge.forward { (start, end) } else { (end, start) };
2675                remaining.push((coedge.clone(), from, to, index));
2676            }
2677            let mut loose: Vec<&(u64, u64, u64, u64, NurbsCurve)> = pieces.clone();
2678            // Trace closed walks: start at a remaining coedge, follow the
2679            // continuation at each vertex — the old loop's own successor when
2680            // it is there (a periodic face's loop passes its seam vertex
2681            // twice), else the unique way on.
2682            while !remaining.is_empty() {
2683                // Start on a coedge that is not a degenerate self-loop (a
2684                // periodic face's pole edge runs from a vertex to itself).
2685                let start_at = remaining.iter().position(|(_, from, to, _)| from != to).unwrap_or(0);
2686                let (first, from0, to0, index0) = remaining.remove(start_at);
2687                let mut walk: Vec<CoedgeRecord> = vec![first];
2688                let mut at = to0;
2689                let mut previous = index0;
2690                let mut guard = 0usize;
2691                loop {
2692                    guard += 1;
2693                    if guard > 4096 {
2694                        return Err(not_built(format!("face {face_id}: the re-trace does not close")));
2695                    }
2696                    let mut next_remaining: Vec<usize> = remaining.iter().enumerate().filter(|(_, (_, from, _, _))| *from == at).map(|(k, _)| k).collect();
2697                    let successor_is_start = (previous + 1) % n == index0;
2698                    if at == from0 && (successor_is_start || next_remaining.is_empty()) {
2699                        // Back at the start along the old loop's own order (a
2700                        // seam vertex passed twice does not close it early).
2701                        if at == from0 && successor_is_start {
2702                            break;
2703                        }
2704                        if loose.iter().all(|piece| piece.2 != at && piece.3 != at) {
2705                            break;
2706                        }
2707                    }
2708                    if next_remaining.len() > 1 {
2709                        if let Some(k) = next_remaining.iter().copied().find(|k| remaining[*k].3 == (previous + 1) % n) {
2710                            next_remaining = vec![k];
2711                        }
2712                    }
2713                    let next_pieces: Vec<usize> = loose.iter().enumerate().filter(|(_, piece)| piece.2 == at || piece.3 == at).map(|(k, _)| k).collect();
2714                    match (next_remaining.len(), next_pieces.len()) {
2715                        (1, 0) => {
2716                            let (coedge, _, to, index) = remaining.remove(next_remaining[0]);
2717                            walk.push(coedge);
2718                            at = to;
2719                            previous = index;
2720                        }
2721                        (0, 1) => {
2722                            let piece = loose.remove(next_pieces[0]);
2723                            let (edge_id, from, to, pcurve) = (piece.1, piece.2, piece.3, &piece.4);
2724                            let (start, _) = edge_ends.get(&edge_id).copied()
2725                                .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: footprint edge {edge_id} missing")))?;
2726                            let along = from == at;
2727                            let coedge_forward = if along { start == from } else { start == to };
2728                            walk.push(CoedgeRecord {
2729                                id: take_id(),
2730                                edge_id,
2731                                forward: coedge_forward,
2732                                pcurve: if along { pcurve.clone() } else { pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")? },
2733                            });
2734                            at = if along { to } else { from };
2735                        }
2736                        (r, p) => {
2737                            return Err(not_built(format!("face {face_id}: vertex {at} has {} ways on ({r} boundary, {p} new), not one", r + p)));
2738                        }
2739                    }
2740                }
2741                rebuilt_loops.push(LoopRecord { id: if rebuilt_loops.is_empty() { loop_record.id } else { take_id() }, coedges: walk });
2742            }
2743            if !loose.is_empty() {
2744                return Err(not_built(format!("face {face_id}: {} new boundary piece(s) joined no loop", loose.len())));
2745            }
2746        }
2747        // The first loop keeps the face; every further closed trace of a
2748        // touched loop is a face of its own on the same carrier.
2749        let kept = rebuilt_loops.remove(0);
2750        face.loops = vec![kept];
2751        let surface = face.surface.clone();
2752        let same_sense = face.same_sense;
2753        let base_name = face.name.clone();
2754        for extra in rebuilt_loops {
2755            let name = base_name.as_ref().map(|base| {
2756                (1..).map(|k| format!("{base}_{k}")).find(|candidate| !taken.contains(candidate)).expect("a free name")
2757            });
2758            if let Some(name) = &name {
2759                taken.push(name.clone());
2760            }
2761            new_faces.push(FaceRecord { id: take_id(), surface: surface.clone(), same_sense, loops: vec![extra], name });
2762        }
2763    }
2764    if !new_faces.is_empty() {
2765        let shell_index = result.shells.iter().position(|shell| !shell.faces.is_empty())
2766            .ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: no shell"))?;
2767        result.shells[shell_index].faces.extend(new_faces);
2768    }
2769    result.edges.retain(|edge| !interior.contains(&edge.id));
2770    Ok(())
2771}
2772
2773/// Swap the two parameters of a face: the surface's control net, every
2774/// coedge pcurve, and the sense (S_u × S_v changes sign).  The face's
2775/// geometry is untouched; only its chart is.  Used to hand an exact
2776/// cylinder patch built with its section along v to the analytic recogniser,
2777/// which wants the circle along u.
2778pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
2779    let surface = &face.surface;
2780    let rows_u = surface.control_points.len();
2781    let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
2782    let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
2783    for row in &surface.control_points {
2784        for (j, point) in row.iter().enumerate() {
2785            transposed[j].push(*point);
2786        }
2787    }
2788    face.surface = NurbsSurface::new(
2789        surface.degree_v,
2790        surface.degree_u,
2791        surface.knots_v.clone(),
2792        surface.knots_u.clone(),
2793        transposed,
2794    ).or_refuse(KernelStage::Refine, "new")?;
2795    for loop_record in &mut face.loops {
2796        for coedge in &mut loop_record.coedges {
2797            for control in &mut coedge.pcurve.control_points {
2798                std::mem::swap(&mut control.x, &mut control.y);
2799            }
2800        }
2801    }
2802    face.same_sense = !face.same_sense;
2803    Ok(())
2804}
2805
2806/// Drop every vertex no longer referenced by an edge — the sharp corners the
2807/// blends replaced, and anything freed by a consumed boundary.  Run ONCE after
2808/// a whole group of stripes is in: a corner vertex is still referenced by the
2809/// selected edges that have not been sewn yet.
2810pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
2811    let used: rustc_hash::FxHashSet<u64> = result
2812        .edges
2813        .iter()
2814        .flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
2815        .collect();
2816    result
2817        .vertices
2818        .retain(|candidate| used.contains(&candidate.id));
2819}
2820
2821fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
2822    crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
2823}
2824
2825
2826