Skip to main content

brep_kernel/blending/blend/edge/
open.rs

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