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