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