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