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(®ion.outer, ®ion.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 ®ion.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 ®ion.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