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