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