Skip to main content

brep_kernel/csg/fragment/
face_split.rs

1use crate::{KernelRefusal, KernelStage, OrRefuse};
2use super::*;
3use super::types::{Chain, ChainSource};
4use super::sampling::{enforce_boundary_crossing_parity, refine_near_hints, sag_refine_chain, sample_chain, trimmed_curve};
5use super::loops::{build_loop, edge_for, interior_points, pcurve_for, point_in_polygon, vertex_point};
6/// Imprint id-index shared across every face of one operand. Built ONCE in
7/// `fragment_solid`; without it each face rebuilt `pieces_by_id`/`vertex_by_id`
8/// over ALL pieces (O(F*(P + P*V))) and linear-scanned `by_face`. Lookup-only,
9/// so every face gets the identical pieces/points the scans returned.
10struct FragmentIndex<'a> {
11    by_face: HashMap<(u8, u64), &'a [u64]>,
12    pieces_by_id: HashMap<u64, &'a ImprintPieceRecord>,
13    vertex_points: HashMap<u64, Vec3>,
14}
15
16impl<'a> FragmentIndex<'a> {
17    fn build(imprint: &'a ImprintResultRecord) -> Self {
18        let mut by_face: HashMap<(u8, u64), &'a [u64]> = HashMap::default();
19        for entry in &imprint.by_face {
20            // `.entry().or_insert` keeps the FIRST entry, matching the previous
21            // `by_face.iter().find(...)` (keys are unique in practice anyway).
22            by_face
23                .entry((entry.operand, entry.face_id))
24                .or_insert_with(|| entry.piece_ids.as_slice());
25        }
26        FragmentIndex {
27            by_face,
28            pieces_by_id: imprint
29                .pieces
30                .iter()
31                .map(|piece| (piece.id, piece))
32                .collect(),
33            vertex_points: imprint
34                .vertices
35                .iter()
36                .map(|vertex| (vertex.id, vertex.point))
37                .collect(),
38        }
39    }
40}
41
42/// Back-compat entry point: builds the imprint index for this single face.
43/// `fragment_solid` builds it once and calls `fragment_face_indexed` directly.
44pub fn fragment_face(
45    solid: &BrepSolid,
46    operand: u8,
47    face: &FaceRecord,
48    imprint: &ImprintResultRecord,
49) -> Result<Vec<FaceFragmentRecord>, KernelRefusal> {
50    let index = FragmentIndex::build(imprint);
51    fragment_face_indexed(solid, operand, face, &index)
52}
53
54/// The winding the SOURCE face's own outer loop uses in its (u,v) chart:
55/// `Some(true)` when it is negative (clockwise), `Some(false)` when positive,
56/// `None` when the sign cannot be trusted.
57///
58/// `build_loop` orients a rebuilt fragment loop against the arrangement, whose
59/// outer cycles are ALWAYS positive-area (`arrangement.rs`: a cycle goes to
60/// `positive` only when `area > tol²`, and negative cycles become holes), and
61/// reversed it only for `!same_sense` — *material runs counter-clockwise in
62/// (u,v) exactly when `same_sense`*.
63///
64/// That rule is CORRECT and is this kernel's stated invariant: it is what
65/// `validate_uv_wire` checks (`brep/topology/validate.rs`, "wire winding
66/// disagrees with face sense"), what `watertight_tessellation::periodic::trim`
67/// cites as the authority for the same question, and what nine sites in `src/`
68/// compare a winding against. It is also forced by the definitions — with
69/// `n_s = S_u × S_v` and `n_f = same_sense ? n_s : −n_s`, material-on-the-left
70/// projected into the chart with `n_s` up leaves no freedom.
71///
72/// So a face carrying a CW outer loop with `same_sense = true` is MALFORMED,
73/// not merely unusual. This function exists because one arrives anyway: nothing
74/// refuses it on the way in, and `validate_uv_wire`'s verdict is filed under
75/// `ValidationReport::wire_warnings`, which `validate()` does not return — so
76/// every `validate().is_empty()` in the kernel reads clean on it.
77///
78/// Reading the source winding here is DEFENCE IN DEPTH, not a blessing of the
79/// chart: it stops the boolean refusing on such a face and builds the correct
80/// body instead. It does not make the face correct, and the real defect is
81/// upstream in whatever produced it.
82///
83/// Measured on the 2026-09-21 through-tunnel body: five of the stretched
84/// wedge's six faces wind positive, `Box_PZ` winds −1.2334e3 with `same_sense =
85/// true` (the plain box's `Box_PZ` winds +1.2143e3; `TF3` is what flips it).
86/// Rebuilding `Box_PZ` against the arrangement's CCW convention flips every
87/// coedge on it, which is the six same-sense edges the boolean then refuses on.
88///
89/// Restricted to OPEN charts on purpose. On a closed/periodic chart a raw
90/// shoelace is meaningless without the seam unwrap the arrangement scan does,
91/// and that path already has its own material-orientation authority in
92/// `full_span_loop_area`; leaving periodic faces on the old rule keeps this
93/// change to the case it was measured on. `None` also covers a loop with too
94/// few samples or an area too small to carry a reliable sign — the caller then
95/// keeps today's `!same_sense`.
96fn source_loop_winding_negative(face: &FaceRecord) -> Result<Option<bool>, KernelRefusal> {
97    let closed = face
98        .surface
99        .closed_directions()
100        .or_refuse(KernelStage::Fragment, "closed_directions")?;
101    if closed.0 || closed.1 {
102        return Ok(None);
103    }
104    // The OUTER loop is the one bounding the most parameter area; holes wind
105    // against it. Reading `loops[0]` would trust a convention this function
106    // exists to stop trusting.
107    let mut best: Option<f64> = None;
108    for loop_record in &face.loops {
109        let mut points: Vec<Vec2> = Vec::new();
110        for coedge in &loop_record.coedges {
111            let [d0, d1] = coedge
112                .pcurve
113                .domain()
114                .or_refuse(KernelStage::Fragment, "domain")?;
115            const SAMPLES: usize = 24;
116            for step in 0..SAMPLES {
117                let fraction = step as f64 / SAMPLES as f64;
118                // `d0 -> d1` ALWAYS. A pcurve is stored in its coedge's own
119                // traversal direction -- `reverse_fragment` flips `forward` and
120                // calls `pcurve.reversed()` together, and the loop-polygon
121                // builder below walks every pcurve `d0 -> d1` regardless of
122                // `forward`. Consulting `forward` here would reverse those
123                // segments in place and trace a zig-zag: on the plain box's
124                // `Box_NZ` (60.715 x 20 in parameter space) that yields 607.15,
125                // exactly half the 1214.3 the loop actually bounds, and the
126                // sign of a self-intersecting polygon is not the loop's
127                // winding.
128                let t = d0 + (d1 - d0) * fraction;
129                let point = coedge
130                    .pcurve
131                    .evaluate(t)
132                    .or_refuse(KernelStage::Fragment, "evaluate")?;
133                points.push(Vec2 { x: point.x, y: point.y });
134            }
135        }
136        if points.len() < 3 {
137            continue;
138        }
139        let mut area = 0.0;
140        for (index, point) in points.iter().enumerate() {
141            let next = points[(index + 1) % points.len()];
142            area += 0.5 * (point.x * next.y - next.x * point.y);
143        }
144        if best.map(|current: f64| area.abs() > current.abs()).unwrap_or(true) {
145            best = Some(area);
146        }
147    }
148    // Scale the "is this sign real" floor to the chart, so a millimetre-wide
149    // face is not judged by a millimetre-squared absolute.
150    let u_domain = KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)
151        .or_refuse(KernelStage::Fragment, "new")?
152        .domain();
153    let v_domain = KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)
154        .or_refuse(KernelStage::Fragment, "new")?
155        .domain();
156    let chart = (u_domain[1] - u_domain[0]) * (v_domain[1] - v_domain[0]);
157    let floor = (chart.abs() * 1e-9).max(f64::MIN_POSITIVE);
158    Ok(match best {
159        Some(area) if area.abs() > floor => Some(area < 0.0),
160        _ => None,
161    })
162}
163
164fn fragment_face_indexed(
165    solid: &BrepSolid,
166    operand: u8,
167    face: &FaceRecord,
168    index: &FragmentIndex,
169) -> Result<Vec<FaceFragmentRecord>, KernelRefusal> {
170    let face_key = FaceKey {
171        operand,
172        face_id: face.id,
173    };
174    let piece_ids = index
175        .by_face
176        .get(&(operand, face.id))
177        .copied()
178        .unwrap_or(&[]);
179    if piece_ids.is_empty() {
180        let u = KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u).or_refuse(KernelStage::Fragment, "new")?.domain();
181        let v = KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v).or_refuse(KernelStage::Fragment, "new")?.domain();
182        let uv = Vec2 {
183            x: (u[0] + u[1]) / 2.0,
184            y: (v[0] + v[1]) / 2.0,
185        };
186        if parameter_point_in_face(face, uv, 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")? != PolygonClass::Inside {
187            // Trimmed faces need the arrangement path even without cuts to
188            // find a point in their actual outer loop.
189        } else {
190            let mut extra_test_points = Vec::new();
191            for (fu, fv) in [(0.35, 0.35), (0.65, 0.65)] {
192                let candidate = Vec2 {
193                    x: u[0] + (u[1] - u[0]) * fu,
194                    y: v[0] + (v[1] - v[0]) * fv,
195                };
196                if parameter_point_in_face(face, candidate, 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")? == PolygonClass::Inside {
197                    extra_test_points.push(face.surface.evaluate(candidate.x, candidate.y).or_refuse(KernelStage::Fragment, "evaluate")?);
198                }
199            }
200            return Ok(vec![FaceFragmentRecord {
201                operand,
202                source_face_id: face.id,
203                surface: face.surface.clone(),
204                same_sense: face.same_sense,
205                loops: face
206                    .loops
207                    .iter()
208                    .map(|loop_record| FragmentLoop {
209                        coedges: loop_record
210                            .coedges
211                            .iter()
212                            .map(|coedge| FragmentCoedge {
213                                source: FragmentEdgeSource::Boundary {
214                                    operand,
215                                    edge_id: coedge.edge_id,
216                                },
217                                forward: coedge.forward,
218                                pcurve: coedge.pcurve.clone(),
219                            })
220                            .collect(),
221                    })
222                    .collect(),
223                test_point: face.surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Fragment, "evaluate")?,
224                test_uv: uv,
225                extra_test_points,
226            }]);
227        }
228    }
229
230    let u_domain = KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u).or_refuse(KernelStage::Fragment, "new")?.domain();
231    let v_domain = KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v).or_refuse(KernelStage::Fragment, "new")?.domain();
232    let diagonal =
233        ((u_domain[1] - u_domain[0]).powi(2) + (v_domain[1] - v_domain[0]).powi(2)).sqrt();
234    let arrangement_tolerance = 1e-7f64.max(diagonal * 1e-6);
235    let snap_threshold = 0.1 * diagonal;
236    let junction_radius = (arrangement_tolerance * 50.0).max(diagonal * 1.5e-3);
237    // WRAPPED-BAND RE-BASE: a face on a closed direction whose material is the
238    // wrapped COMPLEMENT of its trim hull (two full-wrap rims at the hull
239    // edges, material crossing the seam between them — trial 35's torus face:
240    // rims at v=0.25/1.0, material v∈[0,0.25]) cannot be arranged in the flat
241    // domain rectangle: a cut through the band ends at v=0 while its junction
242    // partner (the split rim vertex) sits at v=1, so every cut dangles, cycle
243    // extraction fails, and the face is dropped wholesale (one-use cascade).
244    // Re-base into the material-coherent frame [hull_high, hull_low+period]:
245    // shift every chain point on the low side of the hull window (+period), so
246    // rims and cuts junction in ONE unwrapped chart, and wrap region test
247    // points back into the domain on exit. Detection mirrors the imprint's
248    // restricted-carrier wrap guard: the hull-complement strip contains no
249    // loop curves, so one sample at its middle decides materiality exactly.
250    // Escape hatch: BREP_BAND_REBASE=0.
251    let band_rebase: [Option<(f64, f64)>; 2] = {
252        let mut rebase = [None, None];
253        let closed = face.surface.closed_directions().or_refuse(KernelStage::Fragment, "closed_directions")?;
254        if (closed.0 || closed.1) && std::env::var("BREP_BAND_REBASE").as_deref() != Ok("0") {
255            // Per-coedge hulls on each axis, plus whether any ISO-RIM
256            // (a coedge whose pcurve is near-constant on the axis) sits at a
257            // domain edge — the discriminator for the wrapped-band class.
258            // A seam-straddling boundary band (helmet Face_15, whose source
259            // pcurves are already unwrapped past the domain) has no such rim
260            // and its frame is already coherent; it must NOT be re-based.
261            let mut coverage: [Vec<[f64; 2]>; 2] = [Vec::new(), Vec::new()];
262            let mut edge_rim = [false, false];
263            let mut hull = [
264                [f64::INFINITY, f64::NEG_INFINITY],
265                [f64::INFINITY, f64::NEG_INFINITY],
266            ];
267            for coedge in face
268                .loops
269                .iter()
270                .flat_map(|loop_record| &loop_record.coedges)
271            {
272                let mut extent = [
273                    [f64::INFINITY, f64::NEG_INFINITY],
274                    [f64::INFINITY, f64::NEG_INFINITY],
275                ];
276                for control in &coedge.pcurve.control_points {
277                    if control.w.abs() <= 1e-300 {
278                        continue;
279                    }
280                    let point = [control.x / control.w, control.y / control.w];
281                    for axis in 0..2 {
282                        extent[axis][0] = extent[axis][0].min(point[axis]);
283                        extent[axis][1] = extent[axis][1].max(point[axis]);
284                        hull[axis][0] = hull[axis][0].min(point[axis]);
285                        hull[axis][1] = hull[axis][1].max(point[axis]);
286                    }
287                }
288                for axis in 0..2 {
289                    if !extent[axis][0].is_finite() {
290                        continue;
291                    }
292                    let domain = if axis == 0 { u_domain } else { v_domain };
293                    let span = domain[1] - domain[0];
294                    coverage[axis].push(extent[axis]);
295                    let flat = extent[axis][1] - extent[axis][0] <= 1e-3 * span;
296                    let at_edge = (extent[axis][0] - domain[0]).abs() <= 1e-6 * span
297                        || (extent[axis][1] - domain[1]).abs() <= 1e-6 * span;
298                    if flat && at_edge {
299                        edge_rim[axis] = true;
300                    }
301                }
302            }
303            for axis in 0..2 {
304                if !(if axis == 0 { closed.0 } else { closed.1 }) || !edge_rim[axis] {
305                    continue;
306                }
307                let domain = if axis == 0 { u_domain } else { v_domain };
308                let span = domain[1] - domain[0];
309                // Merge the coverage intervals (clamped into the domain) and
310                // find the largest uncovered gap. The gap contains no boundary
311                // curve, so it is uniformly material or uniformly void — one
312                // sample at its middle decides exactly. A strictly INTERIOR
313                // void gap means the material wraps through the domain edge.
314                let mut intervals: Vec<[f64; 2]> = coverage[axis]
315                    .iter()
316                    .map(|window| {
317                        [window[0].max(domain[0]), window[1].min(domain[1])]
318                    })
319                    .filter(|window| window[1] >= window[0])
320                    .collect();
321                intervals.sort_by(|a, b| a[0].total_cmp(&b[0]));
322                let mut gaps: Vec<[f64; 2]> = Vec::new();
323                let mut reach = domain[0];
324                for window in &intervals {
325                    if window[0] > reach {
326                        gaps.push([reach, window[0]]);
327                    }
328                    reach = reach.max(window[1]);
329                }
330                if reach < domain[1] {
331                    gaps.push([reach, domain[1]]);
332                }
333                let other_mid = (hull[1 - axis][0].max(if axis == 0 {
334                    v_domain[0]
335                } else {
336                    u_domain[0]
337                }) + hull[1 - axis][1].min(if axis == 0 {
338                    v_domain[1]
339                } else {
340                    u_domain[1]
341                })) / 2.0;
342                let mut void: Option<[f64; 2]> = None;
343                for gap in gaps {
344                    if gap[1] - gap[0] <= 1e-6 * span {
345                        continue;
346                    }
347                    let mid = (gap[0] + gap[1]) / 2.0;
348                    let sample = if axis == 0 {
349                        Vec2 {
350                            x: mid,
351                            y: other_mid,
352                        }
353                    } else {
354                        Vec2 {
355                            x: other_mid,
356                            y: mid,
357                        }
358                    };
359                    if parameter_point_in_face(face, sample, 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")? != PolygonClass::Inside
360                        && void
361                            .map(|best| gap[1] - gap[0] > best[1] - best[0])
362                            .unwrap_or(true)
363                    {
364                        void = Some(gap);
365                    }
366                }
367                if let Some([g0, g1]) = void {
368                    // The material+boundary arc wraps the domain edge exactly
369                    // when boundary coverage touches BOTH edge representations
370                    // (the on-seam rim at one, the material-side chains at the
371                    // other) with the void between them. An ordinary band
372                    // (material inside the rims) touches only one edge and
373                    // must keep the flat frame.
374                    let touches_low = intervals
375                        .iter()
376                        .any(|window| window[0] <= domain[0] + 1e-6 * span);
377                    let touches_high = intervals
378                        .iter()
379                        .any(|window| window[1] >= domain[1] - 1e-6 * span);
380                    if touches_low && touches_high {
381                        // Shift everything on the low side of the void up one
382                        // period so the band is contiguous in the arrangement
383                        // frame.
384                        rebase[axis] = Some(((g0 + g1) / 2.0, span));
385                    }
386                }
387            }
388        }
389        rebase
390    };
391    let rebase_point = |mut point: Vec2| -> Vec2 {
392        if let Some((cutoff, period)) = band_rebase[0] {
393            if point.x < cutoff {
394                point.x += period;
395            }
396        }
397        if let Some((cutoff, period)) = band_rebase[1] {
398            if point.y < cutoff {
399                point.y += period;
400            }
401        }
402        point
403    };
404    // Region test points computed in a re-based or unwrapped frame must be
405    // folded back into the domain before trim classification / evaluation.
406    // Wrapping is gated on the axis being CLOSED (a coherent flat face never
407    // produces out-of-domain interior points, so this is a no-op there).
408    let wrap_back = {
409        let closed = face.surface.closed_directions().or_refuse(KernelStage::Fragment, "closed_directions")?;
410        move |mut point: Vec2| -> Vec2 {
411            if closed.0 {
412                let span = u_domain[1] - u_domain[0];
413                if point.x < u_domain[0] || point.x > u_domain[1] {
414                    point.x = u_domain[0] + (point.x - u_domain[0]).rem_euclid(span);
415                }
416            }
417            if closed.1 {
418                let span = v_domain[1] - v_domain[0];
419                if point.y < v_domain[0] || point.y > v_domain[1] {
420                    point.y = v_domain[0] + (point.y - v_domain[0]).rem_euclid(span);
421                }
422            }
423            point
424        }
425    };
426    let pieces_by_id = &index.pieces_by_id;
427    let mut cut_hints = Vec::new();
428    let mut active_pieces = Vec::new();
429    let mut cut_keys = HashMap::default();
430    for piece_id in piece_ids {
431        let piece = pieces_by_id
432            .get(piece_id)
433            .ok_or_else(|| KernelRefusal::internal(KernelStage::Fragment, "fragment.face_split", "fragment_face: missing imprint piece"))?;
434        let pcurve = pcurve_for(piece, face_key)?;
435        let [start, end] = pcurve.domain().or_refuse(KernelStage::Fragment, "domain")?;
436        let a = pcurve.evaluate(start).or_refuse(KernelStage::Fragment, "evaluate")?;
437        let b = pcurve.evaluate(end).or_refuse(KernelStage::Fragment, "evaluate")?;
438        let middle = pcurve.evaluate((start + end) / 2.0).or_refuse(KernelStage::Fragment, "evaluate")?;
439        for point in [a, b] {
440            cut_hints.push(Vec2 {
441                x: point.x,
442                y: point.y,
443            });
444        }
445        let quantize = |point: Vec3| {
446            (
447                (point.x / arrangement_tolerance).round() as i64,
448                (point.y / arrangement_tolerance).round() as i64,
449            )
450        };
451        let mut endpoints = [quantize(a), quantize(b)];
452        endpoints.sort();
453        let key = (endpoints[0], quantize(middle), endpoints[1]);
454        if cut_keys.insert(key, *piece_id).is_none() {
455            active_pieces.push(*piece_id);
456        }
457    }
458
459    let mut segments = Vec::new();
460    let mut chains = Vec::new();
461    let mut vertex_images: std::collections::HashMap<String, Vec<Vec2>> =
462        std::collections::HashMap::new();
463    let mut endpoint_images: Vec<(Vec3, Vec2)> = Vec::new();
464    // Each boundary image carries its 3D vertex point so the boundary-side
465    // canonicalization can refuse to unify two DISTINCT boundary vertices.
466    let mut boundary_images: Vec<(Vec2, Vec3)> = Vec::new();
467    // BOUNDARY-VERTEX ANCHOR (t427 box-corner graze). The boundary-side snap
468    // below (rule 3) merges a chain endpoint onto a same-side boundary image
469    // within arr_tol*100 with NO 3D check. For a CUT/section terminus that is
470    // the intended near-tangent graze bridge onto a trim edge. For a BOUNDARY
471    // endpoint it is a bug: two genuinely-distinct same-operand boundary
472    // vertices (t427: box corner v8 at v=68.4851 vs the edge-16 split A at
473    // v=68.4783, 6.8e-3 apart on the u=max domain edge, 6.8e-3 apart in 3D ≫
474    // the 1e-5 weld floor) get unified, which droops edge 21's pcurve and
475    // mints a phantom corner lens bounded by derived edges → a one-use cluster
476    // that only strands in UNION (where the unsectioned neighbour keeps the
477    // whole edge 21). Anchor a boundary endpoint to a same-side image only when
478    // they are the SAME physical vertex (3D-coincident within the weld floor).
479    // Hatch BREP_BOUNDARY_VERTEX_ANCHOR=0 restores the pre-fix behaviour.
480    let boundary_vertex_anchor_off =
481        std::env::var("BREP_BOUNDARY_VERTEX_ANCHOR").as_deref() == Ok("0");
482    let boundary_side = |point: Vec2| {
483        let distances = [
484            (point.x - u_domain[0]).abs(),
485            (point.x - u_domain[1]).abs(),
486            (point.y - v_domain[0]).abs(),
487            (point.y - v_domain[1]).abs(),
488        ];
489        let index = distances
490            .iter()
491            .enumerate()
492            .min_by(|a, b| a.1.total_cmp(b.1))
493            .map(|entry| entry.0)
494            .unwrap();
495        (distances[index] <= arrangement_tolerance * 50.0).then_some(index)
496    };
497
498    let mut add_chain = |mut points: Vec<Vec2>,
499                         source: ChainSource,
500                         vertex_start_key: String,
501                         vertex_start_point: Vec3,
502                         vertex_end_key: String,
503                         vertex_end_point: Vec3,
504                         is_boundary: bool|
505     -> Result<(), KernelRefusal> {
506        for (at, key, point3) in [
507            (0usize, vertex_start_key, vertex_start_point),
508            (points.len() - 1, vertex_end_key, vertex_end_point),
509        ] {
510            let point = points[at];
511            let images = vertex_images.entry(key).or_default();
512            let canonical = images
513                .iter()
514                .copied()
515                .find(|image| image.sub(point).length() <= snap_threshold)
516                .or_else(|| {
517                    endpoint_images
518                        .iter()
519                        .find(|(candidate3, candidate)| {
520                            candidate3.sub(point3).length() <= 1e-5
521                                && candidate.sub(point).length() <= arrangement_tolerance * 100.0
522                        })
523                        .map(|candidate| candidate.1)
524                })
525                .or_else(|| {
526                    boundary_side(point).and_then(|side| {
527                        boundary_images
528                            .iter()
529                            .find(|(candidate, candidate3)| {
530                                boundary_side(*candidate) == Some(side)
531                                    && candidate.sub(point).length()
532                                        <= arrangement_tolerance * 100.0
533                                    // Boundary endpoints only merge to a same-side
534                                    // image when 3D-coincident (same physical
535                                    // vertex); cut endpoints keep the graze bridge.
536                                    && (boundary_vertex_anchor_off
537                                        || !is_boundary
538                                        || candidate3.sub(point3).length() <= 1e-5)
539                            })
540                            .map(|(candidate, _)| *candidate)
541                    })
542                });
543            if let Some(canonical) = canonical {
544                points[at] = canonical;
545                images.push(canonical);
546            } else {
547                images.push(point);
548                endpoint_images.push((point3, point));
549                if is_boundary && boundary_side(point).is_some() {
550                    boundary_images.push((point, point3));
551                }
552            }
553        }
554        if points.len() > 2 {
555            let start = points[0];
556            let end = points[points.len() - 1];
557            let mut kept = vec![start];
558            kept.extend(points[1..points.len() - 1].iter().copied().filter(|point| {
559                point.sub(start).length() > junction_radius
560                    && point.sub(end).length() > junction_radius
561            }));
562            kept.push(end);
563            points = kept;
564        }
565        let chain_index = chains.len();
566        let chain_segments = points
567            .windows(2)
568            .map(|pair| (pair[0], pair[1]))
569            .collect::<Vec<_>>();
570        for pair in points.windows(2) {
571            segments.push(Segment2 {
572                a: pair[0],
573                b: pair[1],
574                tag: json!({ "chain": chain_index }),
575            });
576        }
577        chains.push(Chain {
578            source,
579            count: points.len() - 1,
580            start: points[0],
581            end: points[points.len() - 1],
582            segments: chain_segments,
583        });
584        Ok(())
585    };
586
587    let _tc = web_time::Instant::now();
588    // SEAM-IMAGE LOOPS: per-loop u-control-hulls, computed once for the
589    // straddling-hole gate below (does any OTHER loop span the full closed-u
590    // domain — the rectangle-with-seam outer that closes the strip walls).
591    let loop_hull_u: Vec<[f64; 2]> = face
592        .loops
593        .iter()
594        .map(|loop_record| {
595            let mut hull = [f64::INFINITY, f64::NEG_INFINITY];
596            for coedge in &loop_record.coedges {
597                for control in &coedge.pcurve.control_points {
598                    if control.w.abs() <= 1e-300 {
599                        continue;
600                    }
601                    hull[0] = hull[0].min(control.x / control.w);
602                    hull[1] = hull[1].max(control.x / control.w);
603                }
604            }
605            hull
606        })
607        .collect();
608    // Signed area of the face's full-u-span loop (the rectangle-with-seam
609    // outer), recorded as it is processed: it defines the chart's material
610    // orientation, so a HOLE is any loop winding OPPOSITE to it (the absolute
611    // sign is chart-dependent — mirrored charts flip both).
612    let mut full_span_loop_area: Option<f64> = None;
613    // Post-rebase boundary polyline segments (incl. seam-image copies), kept
614    // for the sag-refinement boundary-crossing parity guard on cut chains.
615    let mut boundary_segments: Vec<(Vec2, Vec2)> = Vec::new();
616    // The u and v extent of the boundary chains as the arrangement places
617    // them — after the seam-hop unwrap and the band re-base, before any
618    // seam-image copy — which is the period window the face's material lives
619    // in. See the cut-chain period lift below.
620    let mut band_hull = [[f64::INFINITY, f64::NEG_INFINITY]; 2];
621    for (loop_index, loop_record) in face.loops.iter().enumerate() {
622        // SEAM-HOP UNWRAP: a loop whose pcurves are drawn IN-DOMAIN but hop
623        // the seam between consecutive coedges (t91's face 675: the boundary
624        // jumps u=1→0 at v≈0.967 and back at v≈0.453) is incoherent in the
625        // flat chart — its cut chains mint fine and then dangle against the
626        // hopping boundary, cycle extraction fails, and the face drops
627        // wholesale (the trial-35 zero-fragment signature, third variant).
628        // Unwrap onto the covering plane with the SAME per-coedge offsets the
629        // mass integrator / tessellator / seam-band classifier use
630        // (`loop_seam_offsets`); a continuous loop gets all-zero offsets and
631        // is untouched (helmet-class unwrapped sources, native seam-edge
632        // loops, plain faces). Composes with the band re-base above: offsets
633        // normalize the boundary FIRST, after which the re-base's
634        // below-cutoff boundary shifts are no-ops and only its cut-chain
635        // shifts still apply. Escape hatch: BREP_LOOP_UNWRAP=0.
636        let closed = face.surface.closed_directions().or_refuse(KernelStage::Fragment, "closed_directions")?;
637        let offsets = if (closed.0 || closed.1)
638            && std::env::var("BREP_LOOP_UNWRAP").as_deref() != Ok("0")
639        {
640            crate::topology::loop_seam_offsets(
641                &loop_record.coedges,
642                closed.0,
643                closed.1,
644                u_domain[1] - u_domain[0],
645                v_domain[1] - v_domain[0],
646            ).or_refuse(KernelStage::Fragment, "csg.fragment.face_split")?
647        } else {
648            vec![[0.0, 0.0]; loop_record.coedges.len()]
649        };
650        let mut sampled: Vec<(usize, Vec<Vec2>, NurbsCurve, u64, u64)> =
651            Vec::with_capacity(loop_record.coedges.len());
652        for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
653            let edge = edge_for(solid, coedge.edge_id)?;
654            let (start_vertex, end_vertex) = if coedge.forward {
655                (edge.start_vertex_id, edge.end_vertex_id)
656            } else {
657                (edge.end_vertex_id, edge.start_vertex_id)
658            };
659            let offset = offsets
660                .get(coedge_index)
661                .copied()
662                .unwrap_or([0.0, 0.0]);
663            let points: Vec<Vec2> =
664                refine_near_hints(&sample_chain(&coedge.pcurve)?, &coedge.pcurve, &cut_hints)?
665                    .into_iter()
666                    .map(|point| {
667                        rebase_point(Vec2 {
668                            x: point.x + offset[0],
669                            y: point.y + offset[1],
670                        })
671                    })
672                    .collect();
673            let curve = trimmed_curve(&edge.curve, edge.t0, edge.t1)?;
674            let curve = if coedge.forward {
675                curve
676            } else {
677                curve.reversed().or_refuse(KernelStage::Fragment, "reversed")?
678            };
679            sampled.push((coedge_index, points, curve, start_vertex, end_vertex));
680        }
681        // SEAM-IMAGE for a STRADDLING HOLE loop: an inner (hole) loop whose
682        // unwrapped chains poke past a closed-u domain wall exists at only ONE
683        // period image in the arrangement strip, so the strip's OTHER side
684        // never sees it — the seam-adjacent region there keeps a boundary
685        // running straight through the hole opening (t181/00000312#p7: face
686        // 519's decagon hole unwraps to u∈[−0.039,0.039]; the region left of
687        // the seam used the FULL seam edge 475 while the right region split at
688        // the hole crossings; the hole's far-half rim edges stranded one-use
689        // and an 18mm derived chord bridged the gap). Insert a duplicate of
690        // the loop's chains shifted one period toward the strip's other side,
691        // so BOTH walls are split by the hole and the half-hole phantom
692        // regions (rejected by the cross-frame containment) carve it out.
693        // STRUCTURAL GATE (all required): u-closed face; ≥2 loops; the loop's
694        // unwrapped span crosses a u-wall by > junction slack; loop width ≤
695        // half the period (a compact hole, never a rim/horizon); winding
696        // OPPOSITE to the full-span outer loop (hole orientation relative to
697        // the chart — an outer loop copy would double-cover material); and
698        // some OTHER loop's control hull spans the full u-domain (the
699        // seam-carrying rectangle exists). Escape hatch:
700        // BREP_SEAM_IMAGE_LOOPS=0.
701        let image_shift: Option<f64> = {
702            let span = u_domain[1] - u_domain[0];
703            let slack = arrangement_tolerance * 50.0;
704            if !closed.0
705                || face.loops.len() < 2
706                || span <= 0.0
707                || std::env::var("BREP_SEAM_IMAGE_LOOPS").as_deref() == Ok("0")
708            {
709                None
710            } else {
711                let mut min_u = f64::INFINITY;
712                let mut max_u = f64::NEG_INFINITY;
713                let mut area = 0.0;
714                let mut flat: Vec<Vec2> = sampled
715                    .iter()
716                    .flat_map(|(_, points, ..)| points.iter().copied())
717                    .collect();
718                if flat.len() < 3 {
719                    flat.clear();
720                }
721                for (index, point) in flat.iter().enumerate() {
722                    min_u = min_u.min(point.x);
723                    max_u = max_u.max(point.x);
724                    let next = flat[(index + 1) % flat.len()];
725                    area += point.x * next.y - next.x * point.y;
726                }
727                // Full-span discrimination must use the UNWRAPPED width: a
728                // straddling hole's raw control hull spans the whole domain
729                // (its halves are stored at both seam sides), but its
730                // unwrapped chains are compact; only the true outer rectangle
731                // stays period-wide after unwrapping.
732                let spans_full_domain =
733                    !flat.is_empty() && max_u - min_u >= span * (1.0 - 1e-6);
734                if spans_full_domain && full_span_loop_area.is_none() {
735                    full_span_loop_area = Some(area);
736                }
737                let straddles_low = min_u < u_domain[0] - slack;
738                let straddles_high = max_u > u_domain[1] + slack;
739                let compact = max_u - min_u <= 0.5 * span;
740                let is_hole = full_span_loop_area
741                    .map(|outer| !spans_full_domain && area * outer < 0.0)
742                    .unwrap_or(false);
743                let has_full_span_other = loop_hull_u.iter().enumerate().any(|(other, hull)| {
744                    other != loop_index
745                        && hull[0] <= u_domain[0] + 1e-6 * span
746                        && hull[1] >= u_domain[1] - 1e-6 * span
747                });
748                if compact && is_hole && has_full_span_other && (straddles_low ^ straddles_high) {
749                    if std::env::var("BREP_DEBUG_FRAG")
750                        .map(|value| value == face.id.to_string())
751                        .unwrap_or(false)
752                    {
753                        eprintln!(
754                            "  seam-image: face {} loop {loop_index} straddles u-wall (u=[{min_u:.6},{max_u:.6}]) — inserting {} period image",
755                            face.id,
756                            if straddles_low { "+1" } else { "-1" }
757                        );
758                    }
759                    Some(if straddles_low { span } else { -span })
760                } else {
761                    None
762                }
763            }
764        };
765        for (coedge_index, points, curve, start_vertex, end_vertex) in sampled {
766            let coedge = &loop_record.coedges[coedge_index];
767            for point in &points {
768                band_hull[0] = [band_hull[0][0].min(point.x), band_hull[0][1].max(point.x)];
769                band_hull[1] = [band_hull[1][0].min(point.y), band_hull[1][1].max(point.y)];
770            }
771            boundary_segments.extend(points.windows(2).map(|pair| (pair[0], pair[1])));
772            add_chain(
773                points.clone(),
774                ChainSource::Boundary {
775                    coedge: coedge.clone(),
776                    curve: curve.clone(),
777                },
778                format!("b:{operand}:{start_vertex}"),
779                vertex_point(solid, start_vertex)?,
780                format!("b:{operand}:{end_vertex}"),
781                vertex_point(solid, end_vertex)?,
782                true,
783            )?;
784            if let Some(shift) = image_shift {
785                let shifted: Vec<Vec2> = points
786                    .iter()
787                    .map(|point| Vec2 {
788                        x: point.x + shift,
789                        y: point.y,
790                    })
791                    .collect();
792                boundary_segments
793                    .extend(shifted.windows(2).map(|pair| (pair[0], pair[1])));
794                add_chain(
795                    shifted,
796                    ChainSource::Boundary {
797                        coedge: coedge.clone(),
798                        curve,
799                    },
800                    format!("b:{operand}:{start_vertex}"),
801                    vertex_point(solid, start_vertex)?,
802                    format!("b:{operand}:{end_vertex}"),
803                    vertex_point(solid, end_vertex)?,
804                    true,
805                )?;
806            }
807        }
808    }
809    // CUT-CHAIN CLAMP BOUND: on a CLOSED direction the face's boundary band may
810    // legitimately extend past the surface domain — a seam-straddling face is
811    // arranged in UNWRAPPED coordinates (helmet Face_15's boundary chains run
812    // u∈[-0.178, 0.295] on a [0,1]-domain surface). Clamping a cut pcurve to
813    // the surface domain there tears it off the band: the branch-preserved
814    // truncation bridge collapsed to a count=1 sliver hugging u=0 and the face
815    // never split. Widen the clamp bound to domain ∪ trim-pcurve control hulls
816    // (the hulls bound the boundary chains); a cut already inside the domain is
817    // untouched, and OPEN directions keep the plain domain clamp. Escape hatch
818    // BREP_FRAG_ENV_CLAMP=0.
819    let (clamp_u, clamp_v) = {
820        let mut clamp_u = u_domain;
821        let mut clamp_v = v_domain;
822        let (closed_u, closed_v) = face.surface.closed_directions().or_refuse(KernelStage::Fragment, "closed_directions")?;
823        // NOTE (2026-09-16): this hatch's ONLY tamper guard,
824        // `csg/fragment/tests.rs::seam_band_cut_chain_splits_periodic_face`, went
825        // INERT when the march-window lift landed — that test now passes with this
826        // hatch off, so nothing tamper-verifies this mechanism any more. Not that it
827        // is untested: no other test NAMES this hatch.
828        if (closed_u || closed_v) && std::env::var("BREP_FRAG_ENV_CLAMP").as_deref() != Ok("0") {
829            for loop_record in &face.loops {
830                for coedge in &loop_record.coedges {
831                    for point in &coedge.pcurve.control_points {
832                        if point.w.abs() <= 1e-300 {
833                            continue;
834                        }
835                        if closed_u {
836                            clamp_u[0] = clamp_u[0].min(point.x / point.w);
837                            clamp_u[1] = clamp_u[1].max(point.x / point.w);
838                        }
839                        if closed_v {
840                            clamp_v[0] = clamp_v[0].min(point.y / point.w);
841                            clamp_v[1] = clamp_v[1].max(point.y / point.w);
842                        }
843                    }
844                }
845            }
846        }
847        (clamp_u, clamp_v)
848    };
849    for piece_id in active_pieces {
850        let piece = pieces_by_id[&piece_id];
851        let pcurve = pcurve_for(piece, face_key)?.clone();
852        // SAG-BOUNDED CUT-CHAIN FIDELITY (t70 split-set asynchrony class).
853        // `sample_chain`'s uniform sampling is knot-driven, so the SAME
854        // imprint piece gets a 16-segment polyline on one support face and a
855        // 92-segment one on the other (the counts follow each face's pcurve
856        // knot structure, not the curvature). A coarse chord can then sag
857        // MILLIMETRE-scale away from the true pcurve and invade a trim
858        // region the true curve clears — t70: piece 55's chord on face
859        // 1:405 sagged ~3e-3 into a hole corner the true pcurve misses by
860        // 1.3e-3, so the arrangement carved phantom micro-junctions on that
861        // face only, the two owners' split sets diverged (whole Imprint
862        // edge vs a 4-piece Derived chain of the same locus), and every op
863        // failed with one-use edges. Refining every cut chain until its
864        // chords hug the true pcurve within the arrangement's own tolerance
865        // makes both owners' polylines faithful to the ONE shared curve, so
866        // their junction sets agree structurally (no proximity decisions
867        // anywhere). Escape hatch: BREP_CHAIN_SAG_REFINE=0.
868        let (mut points, original_flags) = sag_refine_chain(
869            &pcurve,
870            sample_chain(&pcurve)?,
871            arrangement_tolerance,
872        )?;
873        for point in &mut points {
874            if point.x < clamp_u[0] {
875                point.x = clamp_u[0] + arrangement_tolerance * 10.0;
876            } else if point.x > clamp_u[1] {
877                point.x = clamp_u[1] - arrangement_tolerance * 10.0;
878            }
879            if point.y < clamp_v[0] {
880                point.y = clamp_v[0] + arrangement_tolerance * 10.0;
881            } else if point.y > clamp_v[1] {
882                point.y = clamp_v[1] - arrangement_tolerance * 10.0;
883            }
884        }
885        let snap_endpoint_to_domain = |point: Vec2| {
886            let candidates = [
887                (
888                    (point.x - u_domain[0]).abs(),
889                    Vec2 {
890                        x: u_domain[0],
891                        y: point.y,
892                    },
893                ),
894                (
895                    (point.x - u_domain[1]).abs(),
896                    Vec2 {
897                        x: u_domain[1],
898                        y: point.y,
899                    },
900                ),
901                (
902                    (point.y - v_domain[0]).abs(),
903                    Vec2 {
904                        x: point.x,
905                        y: v_domain[0],
906                    },
907                ),
908                (
909                    (point.y - v_domain[1]).abs(),
910                    Vec2 {
911                        x: point.x,
912                        y: v_domain[1],
913                    },
914                ),
915            ];
916            let nearest = candidates
917                .into_iter()
918                .min_by(|first, second| first.0.total_cmp(&second.0))
919                .unwrap();
920            if nearest.0 <= arrangement_tolerance * 50.0 {
921                nearest.1
922            } else {
923                point
924            }
925        };
926        let last = points.len() - 1;
927        points[0] = snap_endpoint_to_domain(points[0]);
928        points[last] = snap_endpoint_to_domain(points[last]);
929        for point in &mut points {
930            *point = rebase_point(*point);
931        }
932        // CUT-CHAIN PERIOD LIFT. A section's pcurve on a closed carrier is
933        // drawn on the carrier's own period, but the boundary chains above can
934        // sit a period away from it: an imported face whose seam edge is not
935        // at the carrier's parameter origin carries rims split AT that origin,
936        // so its loop hops the branch cut between coedges and the seam-hop
937        // unwrap lays it out over [seam, seam + period] (the 2026-09-14 gear:
938        // cone and fillet torus with seam edges at u = 1/2, boundary chains on
939        // u ∈ [0.5, 1.5], section arcs on [0, 0.5] and [0.5, 1]). The arc a
940        // period off never meets the band, the section never closes across the
941        // face, the face stays one fragment and is kept or dropped whole — one
942        // rim and the section ring then strand one-use. Every piece the imprint
943        // attaches to a face has its midpoint inside that face's trim, so the
944        // chain's image whose middle lies inside the band is the one the
945        // arrangement must see; a whole-period shift is exact on a periodic
946        // carrier. The chain is lifted only when its middle is OUTSIDE the band
947        // and exactly one period image puts it inside, so a chain already
948        // coherent with its band — every native face, every re-based band —
949        // is untouched. The pcurve keeps the carrier's own period (the lift is
950        // an arrangement coordinate, recorded on the chain for the partial-run
951        // rescue). A lifted chain that still crosses a wall of the band crosses
952        // the face's seam edge unsplit, and the face refuses by name. Escape
953        // hatch `BREP_CUT_PERIOD_LIFT=0`.
954        let mut lift = Vec2 { x: 0.0, y: 0.0 };
955        if std::env::var("BREP_CUT_PERIOD_LIFT").as_deref() != Ok("0") {
956            let closed = face.surface.closed_directions().or_refuse(KernelStage::Fragment, "closed_directions")?;
957            for axis in 0..2 {
958                let (is_closed, domain) = if axis == 0 { (closed.0, u_domain) } else { (closed.1, v_domain) };
959                let [low, high] = band_hull[axis];
960                let span = domain[1] - domain[0];
961                if !is_closed || !(span > 0.0) || !(low < domain[0] || high > domain[1]) {
962                    continue;
963                }
964                let coordinate = |point: &Vec2| if axis == 0 { point.x } else { point.y };
965                let middle = coordinate(&points[points.len() / 2]);
966                let inside = |value: f64| value > low && value < high;
967                if inside(middle) {
968                    continue;
969                }
970                let images: Vec<f64> = [-2.0, -1.0, 1.0, 2.0]
971                    .into_iter()
972                    .map(|periods| periods * span)
973                    .filter(|shift| inside(middle + shift))
974                    .collect();
975                let [shift] = images[..] else {
976                    continue;
977                };
978                let slack = arrangement_tolerance * 50.0;
979                let (reach_low, reach_high) = points.iter().map(coordinate).fold(
980                    (f64::INFINITY, f64::NEG_INFINITY),
981                    |(lo, hi), value| (lo.min(value), hi.max(value)),
982                );
983                if reach_low + shift < low - slack || reach_high + shift > high + slack {
984                    let [first, second] = piece.support_faces;
985                    return Err(KernelRefusal::internal(
986                        KernelStage::Fragment,
987                        "fragment.face_split",
988                        format!(
989                            "fragment_face: section piece {piece_id} of faces {}:{} x {}:{} crosses \
990                             the seam wall of face {}:{}'s unwrapped band ({} in [{low:.9}, {high:.9}], \
991                             the section's period image [{:.9}, {:.9}]) without a vertex there",
992                            first.operand,
993                            first.face_id,
994                            second.operand,
995                            second.face_id,
996                            operand,
997                            face.id,
998                            if axis == 0 { "u" } else { "v" },
999                            reach_low + shift,
1000                            reach_high + shift,
1001                        ),
1002                    ));
1003                }
1004                for point in &mut points {
1005                    if axis == 0 {
1006                        point.x += shift;
1007                    } else {
1008                        point.y += shift;
1009                    }
1010                }
1011                if axis == 0 {
1012                    lift.x = shift;
1013                } else {
1014                    lift.y = shift;
1015                }
1016                if std::env::var("BREP_DEBUG_FRAG")
1017                    .map(|value| value == face.id.to_string())
1018                    .unwrap_or(false)
1019                {
1020                    eprintln!(
1021                        "  period lift: face {} cut p{piece_id} {} {shift:+} into the band [{low:.6},{high:.6}]",
1022                        face.id,
1023                        if axis == 0 { "u" } else { "v" }
1024                    );
1025                }
1026            }
1027        }
1028        // Guard runs on the FINAL (clamped/snapped/rebased) coordinates — the
1029        // same frame the boundary chains were sampled into — so crossing
1030        // parity is measured exactly where the arrangement would see it.
1031        points = enforce_boundary_crossing_parity(
1032            points,
1033            &original_flags,
1034            &boundary_segments,
1035            arrangement_tolerance,
1036            std::env::var("BREP_DEBUG_FRAG")
1037                .map(|value| value == face.id.to_string())
1038                .unwrap_or(false),
1039            face.id,
1040        );
1041        let start_point = index
1042            .vertex_points
1043            .get(&piece.start_vertex_id)
1044            .copied()
1045            .ok_or_else(|| KernelRefusal::internal(KernelStage::Fragment, "fragment.face_split", "fragment_face: missing imprint start vertex"))?;
1046        let end_point = index
1047            .vertex_points
1048            .get(&piece.end_vertex_id)
1049            .copied()
1050            .ok_or_else(|| KernelRefusal::internal(KernelStage::Fragment, "fragment.face_split", "fragment_face: missing imprint end vertex"))?;
1051        add_chain(
1052            points,
1053            ChainSource::Cut {
1054                piece_id,
1055                pcurve,
1056                lift,
1057                curve: trimmed_curve(&piece.curve, piece.t0, piece.t1)?,
1058                shared_ring: piece.shared_edge.filter(|_| {
1059                    piece
1060                        .curve
1061                        .evaluate(piece.t0)
1062                        .ok()
1063                        .zip(piece.curve.evaluate(piece.t1).ok())
1064                        .is_some_and(|(a, b)| a.sub(b).length() <= 1e-6)
1065                }),
1066            },
1067            format!("i:{}", piece.start_vertex_id),
1068            start_point,
1069            format!("i:{}", piece.end_vertex_id),
1070            end_point,
1071            false,
1072        )?;
1073    }
1074    drop(add_chain);
1075
1076    // POLE-GAP BRIDGE (single-loop periodic cap; corpus fixture 25 cube-pierce).
1077    // A face closed in one parameter direction can carry a DEGENERATE pole
1078    // iso-line — a whole domain-edge row of one parameter collapsing to a
1079    // single 3D point (the dome apex v2 at v=0 here). The imported trim
1080    // represents the pole with a partial arc plus a degenerate self-edge that
1081    // stop SHORT of the opposite seam foot, so the outer boundary is OPEN in
1082    // the flat UV chart at the pole: the two seam feet sit a FRACTION of a
1083    // period apart (never a whole period), so no covering-strip period-image
1084    // can ever join them — this case is structurally outside the seam-image
1085    // loops machinery above. When a section cut then splits the cap,
1086    // `arrange_segments` closes only the lens region that avoids the pole and
1087    // DROPS the seam-and-pole-spanning outside region: the cap fragments to 1
1088    // not 2, its section + outer-rim edges strand one-use (union/subtract fail
1089    // `S=1 genus=1`), while intersect keeps the lens and succeeds. Close the
1090    // boundary in-chart: pair the two dangling (odd-incidence) boundary
1091    // endpoints that are UV images of the SAME solid vertex (structural
1092    // identity via `vertex_images`, never proximity clustering), VERIFY the
1093    // straight UV connector between them is degenerate (every sample collapses
1094    // to one 3D point — a genuine pole, not a real seam that would cross
1095    // material), and add a bridge chain that REUSES an existing degenerate
1096    // pole edge as its source. The reused edge is 3D-zero-length, so the
1097    // minted coedge is s==e and exempt from the two-use rule — no new edge is
1098    // introduced (the zero-regression reuse pattern), and the outside fragment
1099    // reproduces the original cap loop's own pole/seam structure. A
1100    // cleanly-closing boundary has no odd node and is byte-identical untouched;
1101    // a genuine (non-degenerate) seam gap fails the degeneracy check and is
1102    // left alone (the safe direction). Escape hatch: BREP_POLE_GAP_BRIDGE=0.
1103    let pole_gap_closed = face.surface.closed_directions().or_refuse(KernelStage::Fragment, "closed_directions")?;
1104    if (pole_gap_closed.0 || pole_gap_closed.1)
1105        && std::env::var("BREP_POLE_GAP_BRIDGE").as_deref() != Ok("0")
1106    {
1107        // A degenerate pole iso-line only exists on a surface closed in the
1108        // seam direction; a non-periodic face never reaches this block and is
1109        // byte-identical untouched.
1110        //
1111        // `add_chain` canonicalizes shared endpoints to a bit-identical `Vec2`,
1112        // so a TIGHT epsilon groups exactly the coincident nodes and never
1113        // over-merges two distinct nearby vertices (which would corrupt the
1114        // incidence parity on a small face).
1115        let node_tol = arrangement_tolerance * 10.0;
1116        // Incidence over BOUNDARY chain endpoints only — the pole gap is a
1117        // property of the trim boundary, and cut chains anchor to it elsewhere.
1118        let mut nodes: Vec<(Vec2, usize)> = Vec::new();
1119        for chain in &chains {
1120            if !matches!(chain.source, ChainSource::Boundary { .. }) {
1121                continue;
1122            }
1123            for point in [chain.start, chain.end] {
1124                if let Some(entry) = nodes.iter_mut().find(|(q, _)| q.sub(point).length() <= node_tol)
1125                {
1126                    entry.1 += 1;
1127                } else {
1128                    nodes.push((point, 1));
1129                }
1130            }
1131        }
1132        // Odd incidence => the boundary is UV-open at that endpoint.
1133        let odd: Vec<Vec2> = nodes
1134            .iter()
1135            .filter(|(_, count)| count % 2 == 1)
1136            .map(|(point, _)| *point)
1137            .collect();
1138        // Each odd node's owning solid vertex: the canonical image lists in
1139        // `vertex_images` are keyed "b:{operand}:{vertex_id}".
1140        let vertex_of = |point: Vec2| -> Option<u64> {
1141            vertex_images.iter().find_map(|(key, images)| {
1142                images
1143                    .iter()
1144                    .any(|image| image.sub(point).length() <= node_tol)
1145                    .then(|| key.rsplit(':').next().and_then(|id| id.parse::<u64>().ok()))
1146                    .flatten()
1147            })
1148        };
1149        let mut by_vertex: HashMap<u64, Vec<Vec2>> = HashMap::default();
1150        for point in &odd {
1151            if let Some(vertex_id) = vertex_of(*point) {
1152                by_vertex.entry(vertex_id).or_default().push(*point);
1153            }
1154        }
1155        // Forward-evaluate only (the projector is a poisoned oracle; evaluate
1156        // is exact). A degenerate 3D curve collapses to a point over its span.
1157        let curve_degenerate = |curve: &NurbsCurve| -> bool {
1158            let Ok(domain) = curve.domain() else {
1159                return false;
1160            };
1161            let Ok(base) = curve.evaluate(domain[0]) else {
1162                return false;
1163            };
1164            [0.5, 1.0].iter().all(|fraction| {
1165                curve
1166                    .evaluate(domain[0] + (domain[1] - domain[0]) * fraction)
1167                    .map(|point| point.sub(base).length() <= 1e-5)
1168                    .unwrap_or(false)
1169            })
1170        };
1171        for (vertex_id, images) in by_vertex {
1172            // A pole slit unclosed in exactly one place has EXACTLY two dangling
1173            // images of its apex vertex; anything else is not this class.
1174            if images.len() != 2 {
1175                continue;
1176            }
1177            let (a, b) = (images[0], images[1]);
1178            // The straight UV connector must be a degenerate iso-line: every
1179            // sample collapses to one 3D point. This is the gate that keeps a
1180            // real seam (whose connector crosses material) from being bridged.
1181            let mut base: Option<Vec3> = None;
1182            let mut degenerate = true;
1183            for step in 0..=8 {
1184                let fraction = step as f64 / 8.0;
1185                let uv = a.add(b.sub(a).scale(fraction));
1186                match face.surface.evaluate(uv.x, uv.y) {
1187                    Ok(point) => match base {
1188                        None => base = Some(point),
1189                        Some(reference) => {
1190                            if point.sub(reference).length() > 1e-5 {
1191                                degenerate = false;
1192                                break;
1193                            }
1194                        }
1195                    },
1196                    Err(_) => {
1197                        degenerate = false;
1198                        break;
1199                    }
1200                }
1201            }
1202            if !degenerate {
1203                continue;
1204            }
1205            // Reuse an existing degenerate pole edge's source (its 3D curve is a
1206            // point, so the minted coedge is s==e and validation-exempt).
1207            let Some(source) = chains.iter().find_map(|chain| match &chain.source {
1208                ChainSource::Boundary { coedge, curve }
1209                    if chain.start.sub(chain.end).length() <= node_tol
1210                        && chain.count <= 1
1211                        && (chain.start.sub(a).length() <= node_tol
1212                            || chain.start.sub(b).length() <= node_tol)
1213                        && curve_degenerate(curve) =>
1214                {
1215                    Some(ChainSource::Boundary {
1216                        coedge: coedge.clone(),
1217                        curve: curve.clone(),
1218                    })
1219                }
1220                _ => None,
1221            }) else {
1222                continue;
1223            };
1224            let chain_index = chains.len();
1225            segments.push(Segment2 {
1226                a,
1227                b,
1228                tag: json!({ "chain": chain_index }),
1229            });
1230            chains.push(Chain {
1231                source,
1232                count: 1,
1233                start: a,
1234                end: b,
1235                segments: vec![(a, b)],
1236            });
1237            boundary_segments.push((a, b));
1238            if std::env::var("BREP_DEBUG_FRAG")
1239                .map(|value| value == face.id.to_string())
1240                .unwrap_or(false)
1241            {
1242                eprintln!(
1243                    "  pole-gap bridge: face {} vertex {vertex_id} joins \
1244                     ({:.6},{:.6})->({:.6},{:.6}) along a degenerate iso-line",
1245                    face.id, a.x, a.y, b.x, b.y
1246                );
1247            }
1248        }
1249    }
1250
1251    // A cut chain that coincides with a boundary chain cannot split the
1252    // interior — the face's own edge already bounds it there. Worse, the
1253    // duplicate overlapping segments corrupt arrangement cycle extraction
1254    // and silently swallow every region that touches the overlap (seen when
1255    // endpoint polishing snaps a face/face intersection line onto a
1256    // near-coincident boundary edge of the same face). Drop such cuts
1257    // before arranging.
1258    let coincidence_tolerance = arrangement_tolerance * 50.0;
1259    let point_segment_distance = |point: Vec2, a: Vec2, b: Vec2| -> f64 {
1260        let segment = b.sub(a);
1261        let length_squared = segment.dot(segment);
1262        if length_squared <= 1e-30 {
1263            return point.sub(a).length();
1264        }
1265        let parameter = (point.sub(a).dot(segment) / length_squared).clamp(0.0, 1.0);
1266        point.sub(a.add(segment.scale(parameter))).length()
1267    };
1268    let near_chain = |point: Vec2, chain: &Chain, tolerance: f64| {
1269        chain
1270            .segments
1271            .iter()
1272            .any(|(a, b)| point_segment_distance(point, *a, *b) <= tolerance)
1273    };
1274    // A cut coincides with a boundary only if its INTERIOR follows the same
1275    // path, not merely its endpoints. A single-segment straight cut whose two
1276    // endpoints happen to land on a CURVED boundary edge's endpoints (a
1277    // cap∩cap intersection chord spanning a curved rim arc — the missing
1278    // cone-cap split behind the curved-primitive open-edge booleans) shares
1279    // only its endpoints and cuts clean across the interior; dropping it as a
1280    // duplicate loses the region it carves. So also require the segment
1281    // MIDPOINTS to lie near the boundary. The midpoint test runs at a looser
1282    // threshold than the endpoint match: a genuinely-coincident curved cut's
1283    // polyline midpoints carry their own chord sag (order 1× tolerance), which
1284    // must still count as near, while a chord spanning a curved arc departs by
1285    // the arc's sagitta (orders of magnitude larger) and is correctly kept. On
1286    // the box path both edges are straight, so two shared endpoints force the
1287    // midpoint onto the boundary and the drop is unchanged.
1288    let midpoint_tolerance = coincidence_tolerance * 8.0;
1289    // CORNER-CHORD VETO. The endpoint match above admits a cut whose endpoint
1290    // sits up to `coincidence_tolerance` (50x arr_tol) from the boundary
1291    // chain's endpoint. When a section threads through a MODEL CORNER where two
1292    // boundary edges (legs) meet at a shared vertex, the section arm on the
1293    // sliver face between them is the HYPOTENUSE of a tiny right triangle whose
1294    // short leg can be well under that slack (t634: the torus crosses a 3-face
1295    // corner of 00000327; on the sliver face the arm's start is 31x arr_tol
1296    // from the matched leg's start, and the hypotenuse midpoint is ~15x arr_tol
1297    // off the leg — both under the 50x/400x tolerances). The old test dropped
1298    // that arm as a "duplicate" of one leg, so the tiny corner region was never
1299    // carved on the sliver face while the mate operand kept the arm -> the
1300    // shared section edge went one-use (invalid genus). A genuine corner chord
1301    // is distinguishable STRUCTURALLY, not by tolerance: its endpoint coincides
1302    // (at NODE scale = the arrangement's own find_node snap) with a real,
1303    // DISTINCT boundary vertex — proof it terminates at a 0-cell of its own,
1304    // spanning two legs, rather than retracing this one edge. A true retrace
1305    // (box duplicate, cap-rim chord) has endpoints AT the matched chain's ends
1306    // (gap ~0), so the veto never triggers for it. Hatch restores the old drop.
1307    let corner_veto_enabled = std::env::var("BREP_DUP_DROP_CORNER_VETO")
1308        .map(|value| value != "0")
1309        .unwrap_or(true);
1310    let node_tolerance = arrangement_tolerance;
1311    let terminates_at_distinct_boundary_vertex = |cut_end: Vec2, matched_end: Vec2| -> bool {
1312        cut_end.sub(matched_end).length() > node_tolerance
1313            && chains.iter().any(|other| {
1314                matches!(other.source, ChainSource::Boundary { .. })
1315                    && (other.start.sub(cut_end).length() <= node_tolerance
1316                        || other.end.sub(cut_end).length() <= node_tolerance)
1317            })
1318    };
1319    let debug_drop = std::env::var("BREP_DEBUG_FRAG")
1320        .map(|value| value == face.id.to_string())
1321        .unwrap_or(false);
1322    // MICRO-BOUNDARY SKIP (t316). A boundary chain SHORTER than the coincidence
1323    // tolerance cannot serve as a duplication reference: its two endpoints are
1324    // indistinguishable at this tolerance, so the orientation-agnostic endpoint
1325    // match collapses and a cut that merely touches ONE of its ends spuriously
1326    // "matches" BOTH. That wrongly drops a junction-bridge micro-cut spanning a
1327    // weld-scale vertex cluster (t316: cut p38, the ~0.004mm bridge from a
1328    // section terminus C to the plate-rim node C', gets dropped against e99 — a
1329    // ~0.008mm rim segment shorter than the 0.07mm coincidence tolerance —
1330    // stranding the whole b-poke bottom-cap sliver one-use). The genuine target
1331    // (the tangent-wall cap section, coincident with a full-length box edge ≫
1332    // tolerance) is untouched. Independent of the corner-chord veto below: this
1333    // guard disqualifies a sub-tolerance BOUNDARY as a reference, that one spares
1334    // a corner-spanning CUT. Hatch `BREP_DUP_CUT_MICRO_BOUNDARY=0` restores the
1335    // old, length-agnostic reference test.
1336    let micro_boundary_skip = std::env::var("BREP_DUP_CUT_MICRO_BOUNDARY").as_deref() != Ok("0");
1337    let boundary_extent = |boundary: &Chain| -> f64 {
1338        boundary
1339            .segments
1340            .iter()
1341            .map(|(a, b)| b.sub(*a).length())
1342            .sum()
1343    };
1344    // POLE-COLLAPSED ENDS. A pole is a whole row of the chart at one 3D
1345    // point, so two chain ends on its degenerate boundary chain are one point
1346    // however far apart their u reads, and near the pole a chart distance says
1347    // nothing about the 3D one. The revolve_pole union's G2 end cap (a 93 deg
1348    // sector with its apex on the axis) is cut by the wedge's two side planes
1349    // along its own radius edges: the sections are those edges' sub-arcs to
1350    // 1e-15, ending 3.2e-6 short of the apex. Their pcurves follow u = 1 and
1351    // u = 0 and then swing to one u = 0.443 at the pole; evaluated on the cap
1352    // a chain stands 7.4e-4 off its radius at v = 0.99976. The endpoint test
1353    // read a gap of 0.557 and kept both cuts, and the arrangement carved two
1354    // slivers, each bounded by a radius edge and its own weld copy. One was
1355    // kept in the union and the coplanar merge refused its genus; the
1356    // intersection refused with one-use edges. So when a chain end and a
1357    // boundary end both lie on one degenerate boundary chain they coincide,
1358    // and the cut is held to the boundary in 3D, section curve against edge
1359    // curve, within the coincidence tolerance carried along the boundary by
1360    // its own stretch (3D length over chart length). Escape hatch
1361    // `BREP_DUP_DROP_POLE=0`.
1362    let pole_ends = std::env::var("BREP_DUP_DROP_POLE").as_deref() != Ok("0");
1363    let degenerate_chains: Vec<usize> = chains
1364        .iter()
1365        .enumerate()
1366        .filter(|(_, chain)| match &chain.source {
1367            ChainSource::Boundary { coedge, .. } => solid
1368                .edges
1369                .iter()
1370                .any(|edge| edge.id == coedge.edge_id && edge.degenerate),
1371            ChainSource::Cut { .. } => false,
1372        })
1373        .map(|(index, _)| index)
1374        .collect();
1375    let on_one_pole = |a: Vec2, b: Vec2| -> bool {
1376        degenerate_chains.iter().any(|&pole| {
1377            near_chain(a, &chains[pole], coincidence_tolerance)
1378                && near_chain(b, &chains[pole], coincidence_tolerance)
1379        })
1380    };
1381    let coincident_in_space = |cut: &Chain, boundary: &Chain| -> bool {
1382        let ChainSource::Boundary { curve, .. } = &boundary.source else {
1383            return false;
1384        };
1385        let ChainSource::Cut { curve: section, .. } = &cut.source else {
1386            return false;
1387        };
1388        let Ok([start, end]) = section.domain() else {
1389            return false;
1390        };
1391        let mut chart_length = 0.0;
1392        let mut space_length = 0.0;
1393        for (a, b) in &boundary.segments {
1394            let (Ok(pa), Ok(pb)) = (face.surface.evaluate(a.x, a.y), face.surface.evaluate(b.x, b.y)) else {
1395                return false;
1396            };
1397            chart_length += b.sub(*a).length();
1398            space_length += pb.sub(pa).length();
1399        }
1400        if chart_length <= 0.0 || space_length <= 0.0 {
1401            return false;
1402        }
1403        let bound = coincidence_tolerance * space_length / chart_length;
1404        let stations = cut.segments.len().max(16);
1405        (0..=stations).all(|index| {
1406            section
1407                .evaluate(start + (end - start) * index as f64 / stations as f64)
1408                .ok()
1409                .and_then(|at| crate::project_point_to_curve(curve, at).ok())
1410                .is_some_and(|projection| projection.distance <= bound)
1411        })
1412    };
1413    let mut dropped_chains = HashSet::default();
1414    for (index, chain) in chains.iter().enumerate() {
1415        if !matches!(chain.source, ChainSource::Cut { .. }) {
1416            continue;
1417        }
1418        let mut matched_boundary: Option<usize> = None;
1419        for (boundary_index, boundary) in chains.iter().enumerate() {
1420            if !matches!(boundary.source, ChainSource::Boundary { .. }) {
1421                continue;
1422            }
1423            // t316: a sub-tolerance micro-boundary cannot be a duplication
1424            // reference — its endpoints collapse at this tolerance, so a cut that
1425            // merely touches one end spuriously matches both. Skip it as a
1426            // candidate (never selectable as `matched_boundary`).
1427            if micro_boundary_skip && boundary_extent(boundary) <= coincidence_tolerance {
1428                continue;
1429            }
1430            let forward = boundary.start.sub(chain.start).length() <= coincidence_tolerance
1431                && boundary.end.sub(chain.end).length() <= coincidence_tolerance;
1432            let reversed = boundary.start.sub(chain.end).length() <= coincidence_tolerance
1433                && boundary.end.sub(chain.start).length() <= coincidence_tolerance;
1434            let same_end = |a: Vec2, b: Vec2| a.sub(b).length() <= coincidence_tolerance || on_one_pole(a, b);
1435            let (forward, reversed, at_pole) = if forward || reversed || !pole_ends || degenerate_chains.is_empty() {
1436                (forward, reversed, false)
1437            } else {
1438                let forward = same_end(boundary.start, chain.start) && same_end(boundary.end, chain.end);
1439                let reversed = same_end(boundary.start, chain.end) && same_end(boundary.end, chain.start);
1440                (forward, reversed, true)
1441            };
1442            if !(forward || reversed) {
1443                continue;
1444            }
1445            let interior_coincident = if at_pole {
1446                coincident_in_space(chain, boundary)
1447            } else {
1448                chain.segments.iter().all(|(a, b)| {
1449                    near_chain(*a, boundary, coincidence_tolerance)
1450                        && near_chain(*b, boundary, coincidence_tolerance)
1451                        && near_chain(a.add(*b).scale(0.5), boundary, midpoint_tolerance)
1452                })
1453            };
1454            if !interior_coincident {
1455                continue;
1456            }
1457            if corner_veto_enabled {
1458                // Pair each cut endpoint with the boundary endpoint it matched.
1459                let (bnd_for_start, bnd_for_end) = if forward {
1460                    (boundary.start, boundary.end)
1461                } else {
1462                    (boundary.end, boundary.start)
1463                };
1464                if terminates_at_distinct_boundary_vertex(chain.start, bnd_for_start)
1465                    || terminates_at_distinct_boundary_vertex(chain.end, bnd_for_end)
1466                {
1467                    continue;
1468                }
1469            }
1470            matched_boundary = Some(boundary_index);
1471            break;
1472        }
1473        if let Some(boundary_index) = matched_boundary {
1474            if debug_drop {
1475                let boundary = &chains[boundary_index];
1476                eprintln!(
1477                    "  drop cut chain {} as duplicate of boundary chain {} (endpoint gaps {:.3e}/{:.3e})",
1478                    index,
1479                    boundary_index,
1480                    boundary.start.sub(chain.start).length(),
1481                    boundary.end.sub(chain.end).length(),
1482                );
1483            }
1484            dropped_chains.insert(index);
1485        }
1486    }
1487    if !dropped_chains.is_empty() {
1488        segments.retain(|segment| {
1489            segment.tag["chain"]
1490                .as_u64()
1491                .map(|chain| !dropped_chains.contains(&(chain as usize)))
1492                .unwrap_or(true)
1493        });
1494    }
1495
1496    let debug = std::env::var("BREP_DEBUG_FRAG")
1497        .map(|value| value == face.id.to_string())
1498        .unwrap_or(false);
1499    if debug {
1500        eprintln!(
1501            "fragment_face {}: domain u={:?} v={:?} arr_tol={:.3e} chains={} segments={}",
1502            face.id,
1503            u_domain,
1504            v_domain,
1505            arrangement_tolerance,
1506            chains.len(),
1507            segments.len()
1508        );
1509        for (index, chain) in chains.iter().enumerate() {
1510            let kind = match &chain.source {
1511                ChainSource::Boundary { coedge, .. } => format!("boundary e{}", coedge.edge_id),
1512                ChainSource::Cut { piece_id, .. } => format!("cut p{piece_id}"),
1513            };
1514            eprintln!(
1515                "  chain {index} {kind} count={} start=({:.9},{:.9}) end=({:.9},{:.9})",
1516                chain.count, chain.start.x, chain.start.y, chain.end.x, chain.end.y
1517            );
1518        }
1519    }
1520    // TRIPLE-POINT CUT-CHAIN BRIDGE (imported-geometry corner rescue).
1521    // Two SSI cut curves that meet at a triple point (two faces of one operand
1522    // both crossing a single face of the other) can be marched to endpoints that
1523    // DISAGREE by more than arr_tol when the operand's shared edge carries an
1524    // imported edge<->vertex gap — the imprint then mints TWO vertices for the
1525    // one corner, so the cut chain is BROKEN there. A broken cut cannot separate
1526    // the domain: the arrangement returns one region covering the whole face, the
1527    // face is classified by a single test point and wrongly dropped, and its
1528    // neighbours' shared SSI edges are stranded one-use (non-integral genus).
1529    // Reconnect a pair of cut-chain endpoints that BOTH dangle in the interior
1530    // (each neither on a domain boundary nor meeting any other chain endpoint)
1531    // and whose chains are EACH anchored at their other end, by snapping them to
1532    // their midpoint. Gated on the dangling-anchored-pair precondition, which
1533    // only a broken cut produces, so a face whose cuts already connect (every
1534    // currently-succeeding fragmentation) is never touched.
1535    if chains.iter().any(|c| matches!(c.source, ChainSource::Cut { .. })) {
1536        let endpoints: Vec<(usize, bool, Vec2)> = chains
1537            .iter()
1538            .enumerate()
1539            .flat_map(|(i, c)| [(i, false, c.start), (i, true, c.end)])
1540            .collect();
1541        let anchored = |chain: usize, point: Vec2| -> bool {
1542            boundary_side(point).is_some()
1543                || endpoints.iter().any(|(other_chain, _, other_point)| {
1544                    *other_chain != chain
1545                        && other_point.sub(point).length() <= junction_radius
1546                })
1547        };
1548        // Interior cut endpoints with an anchored far end are the loose ends of
1549        // an almost-through cut. Only these are bridge candidates.
1550        //
1551        // OVERSHOOT-STUB EXCLUSION (offset-shell triple junctions). A pairwise
1552        // section between offset carriers is clipped to the offset IMAGE of the
1553        // source trims, which can extend PAST the true junction with a third
1554        // carrier's section instead of stopping at it (O.S14: the plane×cylinder
1555        // line runs to the source cone∩cylinder circle image at y=100/13 while
1556        // the plane×cone section crosses it ~0.3 earlier at the real offset
1557        // junction). Such a chain is already CONNECTED into the cut graph:
1558        // arrange_segments splits both chains at the crossing node and its
1559        // degree-1 pruning drops the overshoot stub beyond it, and build_loop
1560        // emits the surviving partial runs as Derived sub-curves. The stub's
1561        // dangling endpoint is therefore NOT a broken-junction loose end —
1562        // bridging it would drag a chain end to a midpoint off the section
1563        // (a wrong patch) and the 3D gate would refuse a face the arrangement
1564        // handles exactly. Exclude an endpoint from bridge candidacy when its
1565        // chain intersects another CUT chain at a point interior to the chain
1566        // (beyond the junction radius of both its endpoints — an intersection
1567        // AT the far anchor is the ordinary shared-junction meeting and must
1568        // keep the endpoint eligible, which preserves the imported-geometry
1569        // broken-cut rescue: those chains stop SHORT of each other and have no
1570        // crossing at all).
1571        let cut_chain_has_interior_crossing = |i: usize| -> bool {
1572            let chain = &chains[i];
1573            chains.iter().enumerate().any(|(j, other)| {
1574                // A boundary-duplicate chain was dropped from the arrangement's
1575                // segment set; a crossing with it would not materialize a node.
1576                if j == i
1577                    || dropped_chains.contains(&j)
1578                    || !matches!(other.source, ChainSource::Cut { .. })
1579                {
1580                    return false;
1581                }
1582                chain.segments.iter().any(|&(a0, a1)| {
1583                    other.segments.iter().any(|&(b0, b1)| {
1584                        let Some([t, _]) = crate::arrangement::segment_intersection(
1585                            a0,
1586                            a1,
1587                            b0,
1588                            b1,
1589                            arrangement_tolerance,
1590                        ) else {
1591                            return false;
1592                        };
1593                        let crossing = a0.add(a1.sub(a0).scale(t));
1594                        crossing.sub(chain.start).length() > junction_radius
1595                            && crossing.sub(chain.end).length() > junction_radius
1596                    })
1597                })
1598            })
1599        };
1600        let mut loose: Vec<(usize, bool, Vec2)> = Vec::new();
1601        for (i, c) in chains.iter().enumerate() {
1602            if !matches!(c.source, ChainSource::Cut { .. }) {
1603                continue;
1604            }
1605            for (is_end, this, other) in [(false, c.start, c.end), (true, c.end, c.start)] {
1606                if !anchored(i, this) && anchored(i, other) {
1607                    if cut_chain_has_interior_crossing(i) {
1608                        if debug {
1609                            eprintln!(
1610                                "  cut chain {i} loose {} excluded from junction bridging — \
1611                                 the chain crosses another cut chain mid-run; arrangement \
1612                                 pruning owns the overshoot stub",
1613                                if is_end { "end" } else { "start" },
1614                            );
1615                        }
1616                        continue;
1617                    }
1618                    loose.push((i, is_end, this));
1619                }
1620            }
1621        }
1622        // A triple-point disagreement on imported geometry runs a few 1e-4 (the
1623        // edge<->vertex gap), well under a percent of the face diagonal; bound the
1624        // bridge span so two unrelated loose cuts across a face are never joined.
1625        let bridge_band = (diagonal * 0.02).max(junction_radius * 4.0);
1626        let mut pairs: Vec<(f64, usize, usize)> = Vec::new();
1627        for a in 0..loose.len() {
1628            for b in (a + 1)..loose.len() {
1629                if loose[a].0 == loose[b].0 {
1630                    continue; // never bridge a chain to itself
1631                }
1632                let gap = loose[a].2.sub(loose[b].2).length();
1633                if gap <= bridge_band {
1634                    pairs.push((gap, a, b));
1635                }
1636            }
1637        }
1638        pairs.sort_by(|x, y| x.0.total_cmp(&y.0));
1639        // 3D-DISTANCE CORROBORATION (case 08:10-genus, secondary defect): the
1640        // 2% UV band is metric-blind — on a big-carrier face (full-sphere
1641        // domain [0,1]² spanning hundreds of mm) it silently bridged loose
1642        // ends 8-14 mm apart in 3D, manufacturing UV-closed but 3D-OPEN loops
1643        // ("loop open between coedges" at assembly). A legitimate bridge heals
1644        // a triple-point disagreement (imported edge<->vertex gap, a small
1645        // fraction of the face scale); an impossible one spans a real missing
1646        // piece. Corroborate every candidate pair in 3D: evaluate the surface
1647        // at both (wrapped-back) loose ends and REFUSE — loudly, as a
1648        // missing-piece error — when their 3D gap exceeds 1% of the face's 3D
1649        // diameter (estimated over a coarse full-domain grid; on a small trim
1650        // of a big carrier this overstates the diameter, i.e. errs LENIENT —
1651        // the safe direction). Any probe evaluation failure falls back to the
1652        // old silent bridging (a diagnostic must not fail the operation).
1653        // Escape hatch: BREP_BRIDGE_3D_GATE=0 restores unconditional bridging.
1654        let bridge_3d_gate = std::env::var("BREP_BRIDGE_3D_GATE").as_deref() != Ok("0");
1655        let face_diameter3 = if bridge_3d_gate && !pairs.is_empty() {
1656            let mut samples: Vec<Vec3> = Vec::new();
1657            'grid: for i in 0..3 {
1658                for j in 0..3 {
1659                    let u = u_domain[0] + (u_domain[1] - u_domain[0]) * i as f64 / 2.0;
1660                    let v = v_domain[0] + (v_domain[1] - v_domain[0]) * j as f64 / 2.0;
1661                    match face.surface.evaluate(u, v) {
1662                        Ok(point) => samples.push(point),
1663                        Err(_) => {
1664                            samples.clear();
1665                            break 'grid;
1666                        }
1667                    }
1668                }
1669            }
1670            let mut diameter = 0.0f64;
1671            for (index, a) in samples.iter().enumerate() {
1672                for b in samples.iter().skip(index + 1) {
1673                    diameter = diameter.max(a.sub(*b).length());
1674                }
1675            }
1676            (diameter > 0.0).then_some(diameter)
1677        } else {
1678            None
1679        };
1680        let mut consumed = vec![false; loose.len()];
1681        let mut bridged = 0usize;
1682        for (gap, a, b) in pairs {
1683            if consumed[a] || consumed[b] {
1684                continue;
1685            }
1686            consumed[a] = true;
1687            consumed[b] = true;
1688            let (chain_a, _, point_a) = loose[a];
1689            let (chain_b, _, point_b) = loose[b];
1690            if let Some(diameter) = face_diameter3 {
1691                let wrapped_a = wrap_back(point_a);
1692                let wrapped_b = wrap_back(point_b);
1693                if let (Ok(position_a), Ok(position_b)) = (
1694                    face.surface.evaluate(wrapped_a.x, wrapped_a.y),
1695                    face.surface.evaluate(wrapped_b.x, wrapped_b.y),
1696                ) {
1697                    let gap3 = position_a.sub(position_b).length();
1698                    let limit = 0.01 * diameter;
1699                    if gap3 > limit {
1700                        return Err(KernelRefusal::internal(KernelStage::Fragment, "fragment.face_split", format!(
1701                            "fragment_face {}: cut junction bridge refused — 3D gap \
1702                             {gap3:.4} exceeds {limit:.4} (1% of face diameter \
1703                             {diameter:.4}) across uv gap {gap:.6}; a section piece \
1704                             is missing between chains {chain_a} and {chain_b}",
1705                            face.id
1706                        )));
1707                    }
1708                }
1709            }
1710            let midpoint = point_a.add(point_b).scale(0.5);
1711            for (chain, old) in [(chain_a, point_a), (chain_b, point_b)] {
1712                if chains[chain].start.sub(old).length() <= 1e-15 {
1713                    chains[chain].start = midpoint;
1714                }
1715                if chains[chain].end.sub(old).length() <= 1e-15 {
1716                    chains[chain].end = midpoint;
1717                }
1718                for seg in &mut chains[chain].segments {
1719                    if seg.0.sub(old).length() <= 1e-15 {
1720                        seg.0 = midpoint;
1721                    }
1722                    if seg.1.sub(old).length() <= 1e-15 {
1723                        seg.1 = midpoint;
1724                    }
1725                }
1726                for segment in segments.iter_mut() {
1727                    if segment.tag["chain"].as_u64() != Some(chain as u64) {
1728                        continue;
1729                    }
1730                    if segment.a.sub(old).length() <= 1e-15 {
1731                        segment.a = midpoint;
1732                    }
1733                    if segment.b.sub(old).length() <= 1e-15 {
1734                        segment.b = midpoint;
1735                    }
1736                }
1737            }
1738            bridged += 1;
1739        }
1740        if debug && bridged > 0 {
1741            eprintln!("  bridged {bridged} broken cut junction(s)");
1742        }
1743    }
1744    // GRAZE-BAND COMMON-BLOCK — near-tangent hook absorption (member t548,
1745    // fixture 28). fragment_face runs on the ALREADY edge-split solid, so
1746    // every imprint-minted section×boundary crossing is already a chain endpoint
1747    // by construction. A near-tangent SSI section ending at a shared junction
1748    // vertex can HOOK in its last span: its sampled pcurve overshoots and crosses
1749    // the boundary edge a graze before reaching the shared endpoint (the crossing
1750    // add_edge_split already REFUSED as sub-weld, so the imprint minted only the
1751    // shared vertex). arrange_segments, seeing the linearised hook, re-invents that
1752    // crossing as a distinct INTERIOR point Q, splits the boundary there, and
1753    // spawns a sub-band micro-stub that strands one-use at assembly (t548's
1754    // A→B→C needle). Absorb it deterministically: when a Cut chain and a Boundary
1755    // chain SHARE an endpoint node P, and the cut's polyline crosses that boundary
1756    // at an interior point Q within a MEASURED band of P (3D via surface.evaluate;
1757    // ceiling = the residual-merge sep_cap family, never grown, never the poisoned
1758    // projector), CLIP the cut's hooked tail — drop the samples between the
1759    // crossing and P and run straight into P. The cut's SOURCE curve is untouched,
1760    // so the complete-run path in build_loop still emits the full section edge to
1761    // P; only the spurious arrangement self-crossing is removed. This is the
1762    // arrangement sibling of BREP_SAG_CROSSING_GUARD ("sag refinement may REMOVE a
1763    // crossing but never ADD one"): the arrangement may not invent a boundary
1764    // crossing the imprint did not mint. The structural gate (shared endpoint +
1765    // strictly-interior sub-band crossing) keeps it off legitimate transversals —
1766    // an interior crossing BEYOND the band bails the chain untouched. Generalising
1767    // that gate to Cut×Cut pairs over a measured junction blob was tried and
1768    // falsified as a structural no-op: a crossing in the junction-ADJACENT first
1769    // segment leaves keep_lo=1 reconnecting an identical polyline, and two arms
1770    // that DIVERGE from a shared junction cannot be uncrossed by clipping — this
1771    // absorbs a multi-segment overshoot-and-return hook only. Escape hatch
1772    // BREP_GRAZE_HOOK_CLIP=0.
1773    if std::env::var("BREP_GRAZE_HOOK_CLIP").as_deref() != Ok("0") {
1774        let raw_extent = if solid.vertices.is_empty() {
1775            0.0
1776        } else {
1777            let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
1778            let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
1779            for v in &solid.vertices {
1780                lo.x = lo.x.min(v.point.x);
1781                lo.y = lo.y.min(v.point.y);
1782                lo.z = lo.z.min(v.point.z);
1783                hi.x = hi.x.max(v.point.x);
1784                hi.y = hi.y.max(v.point.y);
1785                hi.z = hi.z.max(v.point.z);
1786            }
1787            hi.sub(lo).length()
1788        };
1789        // Mirror imprint::residual_merge_bands' sep_cap (the measured junction-radius
1790        // family, 1.5e-3·extent, floored by the achieved weld band) — the largest
1791        // near-tangent hook a shared-junction section can plausibly span.
1792        let band = 2.0 * 1e-5 * raw_extent.max(1.0);
1793        let sep_cap = (1.5e-3 * raw_extent).max(band);
1794        let graze_debug = debug || std::env::var("BREP_DEBUG_GRAZE").as_deref() == Ok("1");
1795
1796        let mut plans: Vec<(usize, Vec<(Vec2, Vec2)>, f64)> = Vec::new();
1797        for ci in 0..chains.len() {
1798            if !matches!(chains[ci].source, ChainSource::Cut { .. }) {
1799                continue;
1800            }
1801            // A chain already dropped as a boundary duplicate has had its flat
1802            // `segments` removed; never re-materialise it below.
1803            if dropped_chains.contains(&ci) {
1804                continue;
1805            }
1806            if chains[ci].segments.is_empty() {
1807                continue;
1808            }
1809            let mut lo_cut: Option<usize> = None; // clip a start hook after this seg
1810            let mut hi_cut: Option<usize> = None; // clip an end hook from this seg on
1811            let mut measured = 0.0f64;
1812            let mut bail = false;
1813            for &at_end in &[false, true] {
1814                let p = if at_end {
1815                    chains[ci].end
1816                } else {
1817                    chains[ci].start
1818                };
1819                let Ok(p3) = face.surface.evaluate(p.x, p.y) else {
1820                    continue;
1821                };
1822                for bi in 0..chains.len() {
1823                    if bi == ci || !matches!(chains[bi].source, ChainSource::Boundary { .. }) {
1824                        continue;
1825                    }
1826                    let shares = chains[bi].start.sub(p).length() <= node_tolerance
1827                        || chains[bi].end.sub(p).length() <= node_tolerance;
1828                    if !shares {
1829                        continue;
1830                    }
1831                    for si in 0..chains[ci].segments.len() {
1832                        let (sa, sb) = chains[ci].segments[si];
1833                        for &(ba, bb) in &chains[bi].segments {
1834                            let Some([t1, t2]) = crate::arrangement::segment_intersection(
1835                                sa,
1836                                sb,
1837                                ba,
1838                                bb,
1839                                arrangement_tolerance,
1840                            ) else {
1841                                continue;
1842                            };
1843                            // Strictly interior on BOTH segments: not the shared-node
1844                            // touch (t≈0/1) and not a boundary-endpoint (imprint-minted)
1845                            // crossing (t2≈0/1) — an arrangement-invented self-crossing.
1846                            if !(t1 > 1e-3 && t1 < 1.0 - 1e-3 && t2 > 1e-3 && t2 < 1.0 - 1e-3) {
1847                                continue;
1848                            }
1849                            let q = Vec2 {
1850                                x: sa.x + (sb.x - sa.x) * t1,
1851                                y: sa.y + (sb.y - sa.y) * t1,
1852                            };
1853                            let Ok(q3) = face.surface.evaluate(q.x, q.y) else {
1854                                continue;
1855                            };
1856                            let dist = q3.sub(p3).length();
1857                            if dist > sep_cap {
1858                                // A genuine transversal far from the shared vertex —
1859                                // preserve current behaviour, touch nothing.
1860                                bail = true;
1861                                break;
1862                            }
1863                            measured = measured.max(dist);
1864                            if at_end {
1865                                hi_cut = Some(hi_cut.map_or(si, |m| m.min(si)));
1866                            } else {
1867                                lo_cut = Some(lo_cut.map_or(si, |m| m.max(si)));
1868                            }
1869                        }
1870                        if bail {
1871                            break;
1872                        }
1873                    }
1874                    if bail {
1875                        break;
1876                    }
1877                }
1878                if bail {
1879                    break;
1880                }
1881            }
1882            if bail || (lo_cut.is_none() && hi_cut.is_none()) {
1883                continue;
1884            }
1885            let segs = &chains[ci].segments;
1886            let keep_lo = lo_cut.map(|m| m + 1).unwrap_or(0);
1887            let keep_hi = hi_cut.unwrap_or(segs.len());
1888            if keep_lo >= keep_hi {
1889                continue;
1890            }
1891            let mut points: Vec<Vec2> = Vec::with_capacity(keep_hi - keep_lo + 3);
1892            if lo_cut.is_some() {
1893                points.push(chains[ci].start);
1894            }
1895            points.push(segs[keep_lo].0);
1896            for si in keep_lo..keep_hi {
1897                points.push(segs[si].1);
1898            }
1899            if hi_cut.is_some() {
1900                points.push(chains[ci].end);
1901            }
1902            let new_segments: Vec<(Vec2, Vec2)> = points
1903                .windows(2)
1904                .filter(|w| w[0].sub(w[1]).length() > 0.0)
1905                .map(|w| (w[0], w[1]))
1906                .collect();
1907            if new_segments.is_empty() {
1908                continue;
1909            }
1910            if graze_debug {
1911                eprintln!(
1912                    "graze-hook-clip: face {} cut chain {} measured_band={:.3e} sep_cap={:.3e} \
1913                     segs {} -> {} (lo_cut={:?} hi_cut={:?})",
1914                    face.id,
1915                    ci,
1916                    measured,
1917                    sep_cap,
1918                    chains[ci].segments.len(),
1919                    new_segments.len(),
1920                    lo_cut,
1921                    hi_cut
1922                );
1923            }
1924            plans.push((ci, new_segments, measured));
1925        }
1926        for (ci, new_segments, _) in plans {
1927            chains[ci].count = new_segments.len();
1928            chains[ci].segments = new_segments.clone();
1929            segments.retain(|s| s.tag["chain"].as_u64() != Some(ci as u64));
1930            for (a, b) in new_segments {
1931                segments.push(Segment2 {
1932                    a,
1933                    b,
1934                    tag: json!({ "chain": ci }),
1935                });
1936            }
1937        }
1938    }
1939    let regions = arrange_segments(&segments, arrangement_tolerance).or_refuse(KernelStage::Fragment, "arrange_segments")?;
1940    // Orient every rebuilt loop the way the SOURCE face's own loops are
1941    // oriented, rather than assuming `same_sense` predicts it. The two agree on
1942    // every face whose chart is right-handed against its normal, so this is a
1943    // no-op there; it differs only on a face the old assumption got wrong, and
1944    // getting it wrong reverses that face's whole fragment. Falls back to the
1945    // old rule whenever the source winding cannot be read (see the helper).
1946    // Escape hatch for A/B against the old behaviour.
1947    let source_winding = if std::env::var("BREP_FRAGMENT_SOURCE_WINDING").as_deref() == Ok("0") {
1948        None
1949    } else {
1950        source_loop_winding_negative(face)?
1951    };
1952    let reverse_loops = source_winding.unwrap_or(!face.same_sense);
1953    if debug && reverse_loops != !face.same_sense {
1954        eprintln!(
1955            "  face {}: source loop winds {} against same_sense={} — orienting fragments to the source",
1956            face.id,
1957            if reverse_loops { "CW" } else { "CCW" },
1958            face.same_sense
1959        );
1960    }
1961    let mut fragments = Vec::new();
1962    for region in regions {
1963        let arranged: Vec<Vec2> = interior_points(&region.outer, &region.holes, region.area, 3);
1964        let candidates: Vec<Vec2> = arranged.iter().copied().map(wrap_back).collect();
1965        let Some(&test_uv) = candidates.first() else {
1966            if debug {
1967                eprintln!("  region area={:.6e}: no interior point", region.area);
1968            }
1969            continue;
1970        };
1971        let mut containment = parameter_point_in_face(face, test_uv, 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")?;
1972        // TRIM-FRAME CONTAINMENT. A face whose own pcurves run past its
1973        // carrier's domain (helmet Face_15: its trim spans u in
1974        // [-0.178, 0.298] on a carrier closed in u, drawn without a hop) is
1975        // arranged in that frame, and the region's point is on the face there.
1976        // Its wrapped image (u = 0.970 for -0.030) lies on no trim polygon,
1977        // and containment has no period image to try for a trim that never
1978        // hops the seam, so it reads Outside and the region was dropped: the
1979        // face's y >= 0 piece in `20_helmet_merge_stage` b - a, whose section
1980        // edges then stood one-use. With no band re-base the arrangement's
1981        // frame is the trim's own, so the region's point is asked there as
1982        // well. Escape hatch `BREP_REGION_TRIM_FRAME=0`.
1983        if containment != PolygonClass::Inside
1984            && band_rebase == [None, None]
1985            && (arranged[0].x != test_uv.x || arranged[0].y != test_uv.y)
1986            && std::env::var("BREP_REGION_TRIM_FRAME").as_deref() != Ok("0")
1987        {
1988            containment = parameter_point_in_face(face, arranged[0], 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")?;
1989            if debug {
1990                eprintln!(
1991                    "  region area={:.6e} test=({:.9},{:.9}) asked in the trim's frame at ({:.9},{:.9}): {:?}",
1992                    region.area, test_uv.x, test_uv.y, arranged[0].x, arranged[0].y,
1993                    containment == PolygonClass::Inside
1994                );
1995            }
1996        }
1997        if containment != PolygonClass::Inside {
1998            if debug {
1999                eprintln!(
2000                    "  region area={:.6e} test=({:.9},{:.9}): outside trimmed face",
2001                    region.area, test_uv.x, test_uv.y
2002                );
2003            }
2004            continue;
2005        }
2006        if debug {
2007            eprintln!(
2008                "  region area={:.6e} test=({:.9},{:.9}): KEPT",
2009                region.area, test_uv.x, test_uv.y
2010            );
2011        }
2012        let Some(outer_loop) = build_loop(
2013            &region.outer,
2014            reverse_loops,
2015            &chains,
2016            arrangement_tolerance,
2017            &face.surface,
2018        )?
2019        else {
2020            // Spurious zero-area antenna region (e.g. a seam-grazing fold on a
2021            // periodic band); it bounds no material, so drop it.
2022            if debug {
2023                eprintln!("  region area={:.6e}: dropped (degenerate antenna)", region.area);
2024            }
2025            continue;
2026        };
2027        let mut loops = vec![outer_loop];
2028        for hole in &region.holes {
2029            // Holes take the SAME flag as the outer loop: the arrangement
2030            // hands them back already wound against it, so reversing both
2031            // together keeps them opposite.
2032            if let Some(hole_loop) = build_loop(
2033                hole,
2034                reverse_loops,
2035                &chains,
2036                arrangement_tolerance,
2037                &face.surface,
2038            )? {
2039                loops.push(hole_loop);
2040            }
2041        }
2042        for loop_record in &mut loops {
2043            for coedge in &mut loop_record.coedges {
2044                if let FragmentEdgeSource::Boundary {
2045                    operand: source_operand,
2046                    ..
2047                } = &mut coedge.source
2048                {
2049                    *source_operand = operand;
2050                }
2051            }
2052        }
2053        let mut extra_test_points = Vec::new();
2054        for &candidate in candidates.iter().skip(1) {
2055            if parameter_point_in_face(face, candidate, 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")? == PolygonClass::Inside {
2056                extra_test_points.push(face.surface.evaluate(candidate.x, candidate.y).or_refuse(KernelStage::Fragment, "evaluate")?);
2057            }
2058        }
2059        fragments.push(FaceFragmentRecord {
2060            operand,
2061            source_face_id: face.id,
2062            surface: face.surface.clone(),
2063            same_sense: face.same_sense,
2064            loops,
2065            test_point: face.surface.evaluate(test_uv.x, test_uv.y).or_refuse(KernelStage::Fragment, "evaluate")?,
2066            test_uv,
2067            extra_test_points,
2068        });
2069    }
2070
2071    // Rescue an UNCUT face the flat 2D arrangement failed to reconstruct.
2072    // A seam-closed multi-loop face (a band on a surface of revolution, whose
2073    // param-space loops only close across the u/v seam, or a holed face whose
2074    // domain centre lands in the hole so the fast path above declined it)
2075    // collapses to zero-area lines in the [0,1]^2 arrangement and yields NO
2076    // region -> the face is dropped entirely, stranding its neighbours' shared
2077    // edges as one-use and knocking the assembly's genus off an integer. Since
2078    // the face is uncut, its loops already describe the exact trimmed boundary,
2079    // so pass the unchanged face through — we only need a valid interior test
2080    // point, found by gridding the ACTUAL knot domain. Gated on
2081    // `fragments.is_empty()`, this never touches a face the arrangement handled.
2082    if fragments.is_empty() && piece_ids.is_empty() {
2083        const GRID: usize = 12;
2084        let mut test_uv: Option<Vec2> = None;
2085        let mut extra_test_points: Vec<Vec3> = Vec::new();
2086        'search: for iu in 1..GRID {
2087            for iv in 1..GRID {
2088                let candidate = Vec2 {
2089                    x: u_domain[0] + (u_domain[1] - u_domain[0]) * iu as f64 / GRID as f64,
2090                    y: v_domain[0] + (v_domain[1] - v_domain[0]) * iv as f64 / GRID as f64,
2091                };
2092                if parameter_point_in_face(face, candidate, 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")? == PolygonClass::Inside {
2093                    if test_uv.is_none() {
2094                        test_uv = Some(candidate);
2095                    } else {
2096                        extra_test_points.push(face.surface.evaluate(candidate.x, candidate.y).or_refuse(KernelStage::Fragment, "evaluate")?);
2097                        if extra_test_points.len() >= 2 {
2098                            break 'search;
2099                        }
2100                    }
2101                }
2102            }
2103        }
2104        // SEAM-STRADDLING BAND fallback (a SINGLY-periodic surface of
2105        // revolution whose trim loop closes only ACROSS the u/v seam — e.g. a
2106        // rounded imported cap grazed near-tangent, so it stays uncut). The
2107        // even-odd `parameter_point_in_face` reads Outside EVERYWHERE on such a
2108        // loop (the seam hop makes the [0,1]^2 polygon non-simple), so the knot
2109        // grid above finds nothing and the face would still drop. `seam_band_
2110        // point_in_face` only covers the DOUBLY-periodic (torus) case, so the
2111        // singly-periodic band has no seam-aware classifier. Unwrap the loop
2112        // onto the surface's covering plane (`loop_seam_offsets`, the same fold
2113        // the mass integrator and tessellator use), where it IS a simple
2114        // polygon, grid that unwrapped bbox with the plain even-odd test, and
2115        // map an interior hit back into the domain. Runs ONLY when the grid
2116        // already failed AND the offsets show the loop truly straddled a seam
2117        // (a full-wrap rim yields all-zero offsets and is left untouched), so it
2118        // never changes a face any earlier path handled.
2119        if test_uv.is_none() {
2120            let (closed_u, closed_v) = face.surface.closed_directions().or_refuse(KernelStage::Fragment, "closed_directions")?;
2121            if closed_u || closed_v {
2122                let u_period = u_domain[1] - u_domain[0];
2123                let v_period = v_domain[1] - v_domain[0];
2124                let mut loop_polygons: Vec<Vec<Vec2>> = Vec::new();
2125                let mut straddled = false;
2126                let (mut umin, mut umax, mut vmin, mut vmax) = (
2127                    f64::INFINITY,
2128                    f64::NEG_INFINITY,
2129                    f64::INFINITY,
2130                    f64::NEG_INFINITY,
2131                );
2132                for loop_record in &face.loops {
2133                    let offsets = crate::topology::loop_seam_offsets(
2134                        &loop_record.coedges,
2135                        closed_u,
2136                        closed_v,
2137                        u_period,
2138                        v_period,
2139                    ).or_refuse(KernelStage::Fragment, "csg.fragment.face_split")?;
2140                    if offsets.iter().any(|o| o[0] != 0.0 || o[1] != 0.0) {
2141                        straddled = true;
2142                    }
2143                    let mut polygon = Vec::new();
2144                    for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
2145                        let [d0, d1] = coedge.pcurve.domain().or_refuse(KernelStage::Fragment, "domain")?;
2146                        let samples = 32usize;
2147                        for k in 0..samples {
2148                            let t = d0 + (d1 - d0) * k as f64 / samples as f64;
2149                            let p = coedge.pcurve.evaluate(t).or_refuse(KernelStage::Fragment, "evaluate")?;
2150                            let uv = Vec2 {
2151                                x: p.x + offsets[coedge_index][0],
2152                                y: p.y + offsets[coedge_index][1],
2153                            };
2154                            umin = umin.min(uv.x);
2155                            umax = umax.max(uv.x);
2156                            vmin = vmin.min(uv.y);
2157                            vmax = vmax.max(uv.y);
2158                            polygon.push(uv);
2159                        }
2160                    }
2161                    loop_polygons.push(polygon);
2162                }
2163                if straddled && umax > umin && vmax > vmin {
2164                    let wrap = |value: f64, lo: f64, span: f64| {
2165                        if span > 0.0 {
2166                            lo + (value - lo).rem_euclid(span)
2167                        } else {
2168                            value
2169                        }
2170                    };
2171                    'seam: for iu in 1..GRID {
2172                        for iv in 1..GRID {
2173                            let cu = umin + (umax - umin) * iu as f64 / GRID as f64;
2174                            let cv = vmin + (vmax - vmin) * iv as f64 / GRID as f64;
2175                            let candidate = Vec2 { x: cu, y: cv };
2176                            // Even-odd fill across ALL unwrapped loops: interior
2177                            // of the trimmed region is where the winding is odd
2178                            // (outer loop minus any hole loops), independent of
2179                            // which loop is the outer one.
2180                            let winding = loop_polygons
2181                                .iter()
2182                                .filter(|polygon| point_in_polygon(candidate, polygon))
2183                                .count();
2184                            if winding % 2 == 0 {
2185                                continue;
2186                            }
2187                            let mapped = Vec2 {
2188                                x: if closed_u {
2189                                    wrap(cu, u_domain[0], u_period)
2190                                } else {
2191                                    cu
2192                                },
2193                                y: if closed_v {
2194                                    wrap(cv, v_domain[0], v_period)
2195                                } else {
2196                                    cv
2197                                },
2198                            };
2199                            if test_uv.is_none() {
2200                                test_uv = Some(mapped);
2201                            } else {
2202                                extra_test_points
2203                                    .push(face.surface.evaluate(mapped.x, mapped.y).or_refuse(KernelStage::Fragment, "evaluate")?);
2204                                if extra_test_points.len() >= 2 {
2205                                    break 'seam;
2206                                }
2207                            }
2208                        }
2209                    }
2210                }
2211            }
2212        }
2213        if let Some(uv) = test_uv {
2214            fragments.push(FaceFragmentRecord {
2215                operand,
2216                source_face_id: face.id,
2217                surface: face.surface.clone(),
2218                same_sense: face.same_sense,
2219                loops: face
2220                    .loops
2221                    .iter()
2222                    .map(|loop_record| FragmentLoop {
2223                        coedges: loop_record
2224                            .coedges
2225                            .iter()
2226                            .map(|coedge| FragmentCoedge {
2227                                source: FragmentEdgeSource::Boundary {
2228                                    operand,
2229                                    edge_id: coedge.edge_id,
2230                                },
2231                                forward: coedge.forward,
2232                                pcurve: coedge.pcurve.clone(),
2233                            })
2234                            .collect(),
2235                    })
2236                    .collect(),
2237                test_point: face.surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Fragment, "evaluate")?,
2238                test_uv: uv,
2239                extra_test_points,
2240            });
2241        }
2242    }
2243    Ok(fragments)
2244}
2245
2246pub fn fragment_solid(
2247    solid: &BrepSolid,
2248    operand: u8,
2249    imprint: &ImprintResultRecord,
2250) -> Result<Vec<FaceFragmentRecord>, KernelRefusal> {
2251    let index = FragmentIndex::build(imprint);
2252    let mut fragments = Vec::new();
2253    for face in solid.shells.iter().flat_map(|shell| shell.faces.iter()) {
2254        fragments.extend(fragment_face_indexed(solid, operand, face, &index)?);
2255    }
2256    Ok(fragments)
2257}
2258