brep_kernel/offset/offset_shell/pipeline.rs
1use super::*;
2
3/// Put every face on one normal convention (surface normal on the CCW
4/// parameter-winding side) and then restore outward normals. The offset shell's
5/// source and offset skins are assembled as independent components whose
6/// per-face `same_sense` can follow opposite conventions; `validate()` only
7/// checks coedge-direction coherence (the `forward` flags), so a joined,
8/// watertight, coedge-coherent manifold can still be normal-incoherent and
9/// report the wrong volume. Re-deriving `same_sense` from the loop winding makes
10/// the coedge-coherent manifold normal-coherent; the signed-volume flip then
11/// orients it outward. Only invoked after a non-coplanar rim weld, so the
12/// coplanar cylinder/prism shells keep their exact assembled orientation.
13fn coheres_face_normals(solid: &mut BrepSolid) -> Result<(), String> {
14 for face in solid.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
15 face.same_sense = parameter_space_area(face)? > 0.0;
16 }
17 // Outward is a PER-SHELL property: a through-hole shelled at both ends
18 // legitimately produces two disjoint closed components (the outer wall
19 // tube and the hole wall tube), and a single global signed-volume flip
20 // cannot orient both.
21 for shell_index in 0..solid.shells.len() {
22 let single = BrepSolid {
23 id: solid.id,
24 vertices: solid.vertices.clone(),
25 edges: solid.edges.clone(),
26 shells: vec![solid.shells[shell_index].clone()],
27 genus: 0,
28 };
29 if crate::solid_signed_volume(&single)? < 0.0 {
30 flip_shell_faces(&mut solid.shells[shell_index])?;
31 }
32 }
33 Ok(())
34}
35
36/// Mint a vertex at every SELF-TOUCH of the source's faces before the shell
37/// reads their trims.
38///
39/// A face whose loops touch at an EDGE INTERIOR is a pinched region: the
40/// reported 2026-09-09 document's r = 6 through-hole kisses both r = 4 fillet
41/// setbacks on the top face, at (16, 20, 10) and (10, 20, 16), with no vertex
42/// there. The fragment stage's 2D arrangement resolves such a pinch by
43/// carving the touched-off corner into its own region, whose loop walks the
44/// closed hole chain in a PARTIAL run — which `build_loop` can only assemble
45/// when the touch is a vertex of both touching edges, and otherwise refuses
46/// with "chain N fragmented into an incomplete run".
47///
48/// The boolean lane mints that vertex inside `build_imprints`
49/// (`imprint/self_touch.rs`). Nothing minted it here: the shell fragments
50/// its SOURCE against an EMPTY imprint, and its offset carriers — whose trims
51/// are copied from the source faces — deliberately keep their boundaries
52/// un-split, so both arrived at the arrangement pinched. Splitting the
53/// source once, before the carriers are built from it, fixes both: every
54/// carrier inherits the split trim by construction.
55///
56/// Returns `None` when no face self-touches, so every part that was never
57/// pinched keeps its exact previous path. Escape hatch:
58/// `BREP_SELF_TOUCH_SPLIT=0`, the same one the imprint lane honours.
59fn split_self_touching_source(source: &BrepSolid) -> Result<Option<BrepSolid>, String> {
60 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() == Ok("0") {
61 return Ok(None);
62 }
63 // The same derivation `offset_shell_impl` uses for every other band, so
64 // the source scan and the carrier-pair scans agree on what "touching"
65 // means.
66 let scale = crate::solid_scale(source);
67 let tolerance = 1e-7f64.max(scale * 1e-9);
68 let edge_splits = crate::imprint::self_touch_edge_splits(source, SOURCE_OPERAND, tolerance, scale)?;
69 if edge_splits.is_empty() {
70 return Ok(None);
71 }
72 os_debug!(
73 "self-touch: source split on {} edge(s): {:?}",
74 edge_splits.len(),
75 edge_splits
76 .iter()
77 .map(|split| split.edge_id)
78 .collect::<Vec<_>>(),
79 );
80 let imprint = ImprintResultRecord {
81 edge_splits,
82 ..empty_imprint()
83 };
84 Ok(Some(apply_edge_splits(source, SOURCE_OPERAND, &imprint)?))
85}
86
87/// How far past the grown outline a planar opening wall that meets a RULED
88/// opening is padded, in units of the offset-skin band the wall selection reads
89/// (see the wall construction).
90///
91/// Measured on the torus wedge through the pipeline, grown by 0.05, 0.1, 0.5
92/// and 0.999 (band 2e-3): a clearance of 2 or 2.5 bands still shreds the cap
93/// into 22 to 36 faces with the area 5% to 11% high; from 4 bands on every
94/// member builds its 6 faces, and from 4 to 32 bands each member's volume moves
95/// by at most 1.2e-7. 16 sits four times above the first clean reading.
96const RULED_NEIGHBOUR_CLEARANCE_BANDS: f64 = 16.0;
97
98/// How far past its opening plane an inward carrier is grown, as a fraction of
99/// the shell distance (see the fall-short reach in the carrier loop).
100const REACH_CLEARANCE: f64 = 0.25;
101
102pub fn offset_shell(
103 source: &BrepSolid,
104 opening_face_ids: &[u64],
105 distance: f64,
106) -> Result<OffsetShellResultRecord, String> {
107 // Pinched source faces are un-modellable by the arrangement until the
108 // touch is a vertex; do it once here so BOTH `offset_shell_impl` attempts
109 // and every carrier built from the source see the same topology.
110 let prepared = split_self_touching_source(source)?;
111 let source = prepared.as_ref().unwrap_or(source);
112 // An outward shell of the one circular face of a partial revolve is planar
113 // in the meridian half-plane, fold band and all: build it there and revolve
114 // it once (`revolved_wedge.rs`). Anything else declines and takes the
115 // imprint pipeline, unchanged.
116 match revolved_wedge::recognize(source, opening_face_ids, distance) {
117 Ok(revolved_wedge::Recognition::Wedge(wedge)) => {
118 os_debug!("revolved wedge lane: {}", wedge.report());
119 return revolved_wedge::build(&wedge, source);
120 }
121 Ok(revolved_wedge::Recognition::Declined(reason)) => {
122 os_debug!("revolved wedge lane declined: {reason}");
123 }
124 // A source the lane cannot even read is not one it takes; the pipeline
125 // owns every refusal.
126 Err(error) => {
127 os_debug!("revolved wedge lane declined: could not read the source ({error})");
128 }
129 }
130 imprint_attempts(source, opening_face_ids, distance)
131}
132
133
134fn imprint_attempts(
135 source: &BrepSolid,
136 opening_face_ids: &[u64],
137 distance: f64,
138) -> Result<OffsetShellResultRecord, String> {
139 // First try with oblique hole-wall carriers extended past their opening
140 // planes (the lane that closes tilted/conical through-holes). If that
141 // extension fired but the arrangement could not assemble the harder
142 // configuration it produced (e.g. the extended bore genuinely intersects
143 // other offset walls), fall back to the un-extended pipeline so every
144 // previously-supported shape keeps its exact path and every refusal keeps
145 // the canonical honesty-gate message.
146 let mut extension_fired = false;
147 let mut reflex_rebuilds = 0usize;
148 let mut shadowed_points = Vec::new();
149 let mut omitted = HashSet::default();
150 let mut vacated_notes = Vec::new();
151 let result = match offset_shell_impl(
152 source,
153 opening_face_ids,
154 distance,
155 true,
156 &mut extension_fired,
157 &mut reflex_rebuilds,
158 &HashSet::default(),
159 &mut shadowed_points,
160 &mut omitted,
161 &mut vacated_notes,
162 ) {
163 Ok(result) => {
164 // Attribution marker: which pipeline produced this result. A
165 // retry-produced Ok can silently mask a broken extension lane —
166 // always say which attempt won. The reflex-rebuild count rides
167 // along because that lane fires on BOTH attempts: "extension
168 // idle" alone would misread as "the plain pipeline did this".
169 os_debug!(
170 "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
171 if extension_fired { "fired" } else { "idle" },
172 reflex_rebuilds,
173 );
174 Ok(result)
175 }
176 Err(error) if extension_fired => {
177 os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
178 let mut unused = false;
179 let mut retry_reflex = 0usize;
180 shadowed_points.clear();
181 omitted.clear();
182 let retried = offset_shell_impl(
183 source,
184 opening_face_ids,
185 distance,
186 false,
187 &mut unused,
188 &mut retry_reflex,
189 &HashSet::default(),
190 &mut shadowed_points,
191 &mut omitted,
192 &mut vacated_notes,
193 );
194 os_debug!(
195 "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
196 if retried.is_ok() { "ok" } else { "err" },
197 );
198 retried
199 }
200 Err(error) => Err(error),
201 };
202 // A refusal from a shell whose selection saw two offsets CROSS inside
203 // material thinner than twice the distance says so first: a rib thinner
204 // than 2d shelled inward is that configuration, and the assembly's own
205 // message names only the symptom. A refusal that already names its cause
206 // keeps it. The crossing is looked for here, on the failure path only.
207 match result {
208 // A refusal that already names its CAUSE keeps its own sentence; one
209 // that names a symptom ("connector edge left its plane", an assembly
210 // message) is worth a cause in front of it. Reading the whole prefix as
211 // "anything this module wrote" was too wide: the twin bosses came out
212 // led by a notch sentence when their cause was the shadow cascade, and
213 // the partial cone lost its junction cause behind a connector's.
214 Err(error) if !refusal_names_its_cause(&error) => {
215 if let Some(crossing) =
216 name_offset_crossing(source, opening_face_ids, distance, &shadowed_points)
217 {
218 return Err(format!(
219 "offset_shell: {crossing}; a skin partly shadowed that way is not yet \
220 assembled ({error})"
221 ));
222 }
223 if let Some(junction) = name_retained_opening_junction(source, opening_face_ids, distance)
224 {
225 return Err(format!("offset_shell: {junction} ({error})"));
226 }
227 // A rim the closure could not bridge, with what it measured, in
228 // front of whatever the assembly then said about the hole it left.
229 match vacated_notes.first() {
230 Some(note) => Err(format!(
231 "offset_shell: a rim vacated by an omitted support was left open — {note} \
232 ({error})"
233 )),
234 None => Err(error),
235 }
236 }
237 result => result,
238 }
239}
240
241/// The refusals this module's own cause-naming lanes produce. Anything else is
242/// a symptom another stage reported, and takes a cause in front of it.
243fn refusal_names_its_cause(error: &str) -> bool {
244 [
245 " coincide near ",
246 "cannot tell whether",
247 "omitting the shadowed offsets of source faces",
248 "no retained face keeps any offset skin",
249 "is shadowed at every one of its",
250 "a rim vacated by an omitted support was left open",
251 "junction, so its offset ends inside the cavity",
252 "cross near",
253 // The opening-wall lane's own named refusals, so a cause of mine never
254 // arrives in front of one of theirs.
255 "folds where the carve",
256 "is curved and not a ruled surface",
257 "cannot be built",
258 ]
259 .iter()
260 .any(|name| error.contains(name))
261}
262
263fn offset_shell_impl(
264 source: &BrepSolid,
265 opening_face_ids: &[u64],
266 distance: f64,
267 extend_oblique_carriers: bool,
268 extension_fired: &mut bool,
269 reflex_rebuilds: &mut usize,
270 shadowed_supports: &HashSet<u64>,
271 shadowed_points: &mut Vec<(Vec3, u64)>,
272 omitted_supports: &mut HashSet<u64>,
273 vacated_notes: &mut Vec<String>,
274) -> Result<OffsetShellResultRecord, String> {
275 omitted_supports.clone_from(shadowed_supports);
276 vacated_notes.clear();
277 if !distance.is_finite() || distance.abs() <= MINIMUM_DISTANCE {
278 return Err("offset_shell: distance must be finite and non-zero".into());
279 }
280 if opening_face_ids.is_empty() {
281 return Err("offset_shell: at least one opening face is required".into());
282 }
283 let source_faces = source
284 .shells
285 .iter()
286 .flat_map(|shell| &shell.faces)
287 .collect::<Vec<_>>();
288 let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
289 if opening_set
290 .iter()
291 .any(|id| !source_faces.iter().any(|face| face.id == *id))
292 {
293 return Err("offset_shell: opening face does not belong to source".into());
294 }
295 let retained = source_faces
296 .iter()
297 .filter(|face| !opening_set.contains(&face.id))
298 .map(|face| face.id)
299 .collect::<Vec<_>>();
300 if retained.is_empty() {
301 return Err("offset_shell: removing every face cannot produce a shell".into());
302 }
303 let mut carriers = Vec::new();
304 // Faces whose folded part was carved away, by source face id. Later stages
305 // that read a retained face's TRIM — the apex-cap scan below is the one —
306 // must read the carved trim, not the one the source still carries.
307 let mut carved_supports = HashMap::<u64, FaceRecord>::default();
308
309 for face_id in &retained {
310 let face = source_faces.iter().find(|face| face.id == *face_id).unwrap();
311 if offset_support_collapsed(face, distance)? {
312 os_debug!("omitting collapsed offset support for source face {face_id}");
313 continue;
314 }
315 // A support a previous pass found SHADOWED contributes no skin, and its
316 // carrier must not imprint its neighbours either — see the selection.
317 if shadowed_supports.contains(face_id) {
318 os_debug!("omitting shadowed offset support for source face {face_id}");
319 continue;
320 }
321 // A planar carrier whose source face meets a neighbour at a REFLEX
322 // (concave) edge extends its trim by |distance|: at such a junction
323 // the offset skin must GROW past the source footprint to meet the
324 // neighbour's offset (a pocket wall's source [5,15] → skin
325 // [3.5,16.5]); a source-sized trim leaves a gap no miter can bridge.
326 // The surplus at the face's convex ends is clipped by the imprints +
327 // on-skin filter. Convex-only faces keep their exact source-sized
328 // trim — extending them perturbs delicately-selected fragment
329 // landscapes for no benefit.
330 let miter = source_faces
331 .iter()
332 .find(|face| face.id == *face_id)
333 .map(|face| face_reflex_miter_tan(source, face))
334 .transpose()?
335 .flatten();
336 // The offsets of two faces joined at a reflex edge meet d·tan(θn/2)
337 // past the source rim — d only for a perpendicular join. Scale the
338 // extension to the worst sampled miter (×1.5 headroom so the true
339 // intersection lands strictly INSIDE the extended trim, never on its
340 // boundary where the arrangement drops boundary-coincident cuts) and
341 // cap it: a near-tangential join would ask for an unbounded trim.
342 let reflex_extension = miter.map(|worst_tan| {
343 (distance.abs() * worst_tan * 1.5)
344 .max(distance.abs())
345 .min(distance.abs() * 12.0)
346 });
347 if let Some(extension) = reflex_extension {
348 os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
349 }
350 // OUTWARD: every retained carrier grows by |distance| across its
351 // sharp and opening-facing sides (the offsets of a convex junction
352 // meet past both source rims; a wall plane cuts the surplus), and
353 // by nothing across a smooth junction into a retained neighbour —
354 // see `outward_carrier_extension`. A reflex junction needs no
355 // outward growth at all (those offsets cross inside the trims).
356 let extension = if distance < 0.0 {
357 outward_carrier_extension(source, face, &source_faces, &opening_set, distance.abs())?
358 } else {
359 let mut inward = crate::CarrierExtension::uniform(reflex_extension.unwrap_or(0.0));
360 // An INWARD carrier that falls short of the planar opening it meets
361 // is grown until it crosses that plane, so the imprint cuts the
362 // skin there instead of leaving a rim for the ruled weld to cork
363 // below the opening (`fallshort_opening_reach`).
364 if let Some((at_v_max, reach)) =
365 fallshort_opening_reach(source, face, &source_faces, &opening_set, distance)?
366 {
367 // Past the plane by a quarter of the shell's own thickness, so
368 // the section is strictly inside the carrier's trim rather than
369 // on its boundary. Measured 2026-09-17 over the offset-shell
370 // suite: every fraction from 0.01 to 2.0 of |d| gives the same
371 // 73 passing members, while a margin sized in COINCIDENCE BANDS
372 // (1 to 32 of them, 2e-5 each here) breaks the sphere-under-
373 // frustum member by 9.4e-2 — the fragile margins are the ones
374 // far below the construction's own length.
375 let grown = reach + REACH_CLEARANCE * distance.abs();
376 if at_v_max {
377 inward.v_max = inward.v_max.max(grown);
378 } else {
379 inward.v_min = inward.v_min.max(grown);
380 }
381 os_debug!(
382 "carrier for src face {face_id}: inward reach to its opening plane {reach:.6} at v_{} ",
383 if at_v_max { "max" } else { "min" },
384 );
385 }
386 // A CURVED opening has no such construction, and the pipeline
387 // closes the cavity with whatever completion reaches for: measured
388 // 7.6% short on the hemisphere cup shelled through its dome.
389 if let Some((opening, gap)) =
390 offset_falls_short_of_curved_opening(source, face, &source_faces, &opening_set, distance)?
391 {
392 return Err(format!(
393 "offset_shell: the offset skin of source face {face_id} stops {gap:.3e} short of \
394 curved opening face {opening}; only a planar opening's skin is grown to meet it"
395 ));
396 }
397 inward
398 };
399 if distance < 0.0 {
400 os_debug!(
401 "carrier for src face {face_id}: outward extension u=({:.3},{:.3}) v=({:.3},{:.3})",
402 extension.u_min,
403 extension.u_max,
404 extension.v_min,
405 extension.v_max,
406 );
407 }
408 // CARVE: a support whose offset folds over PART of its trim keeps the
409 // regular part and drops the rest, instead of being built whole with
410 // the fold in it. The carrier is built over the CARVED source, so the
411 // trim it clones stops at the fold locus — and, because that trim no
412 // longer reaches past the offset's pinch, `offset_surface`'s apex-cone
413 // pinch retrim does not fire and the cloned pcurves still name the
414 // pointwise offset.
415 //
416 // A carve that cannot be made is not a licence to build the folded
417 // support anyway: the refusal is the answer, because a carrier with an
418 // inverted patch in it welds into a shell that validates and bounds the
419 // wrong solid.
420 let carved = carve_folded_support(source, face, distance)?;
421 let carrier_source = match &carved {
422 Some(carved) => {
423 os_debug!(
424 "carving the fold off source face {face_id}: {:.1}% of its trim kept, \
425 {} new boundary pcurve(s)",
426 carved.kept_fraction * 100.0,
427 carved.fold_boundary
428 );
429 carved_supports.insert(*face_id, carved.face.clone());
430 &carved.solid
431 }
432 None => source,
433 };
434 let mut carrier = carrier_solid_sided(carrier_source, *face_id, distance, &extension)?;
435 if (distance < 0.0 || reflex_extension.is_some()) && face.surface.is_affine()? {
436 // PADDED planes: the pad slides the plane's net while the trim's
437 // loops are cloned in uv, so an INTERIOR loop (a bore or boss rim
438 // in the face) is carried outward with the grown outline instead
439 // of following the neighbour's offset.
440 // OUTWARD: a bore's rim grew (r=4.68 → 5.15) while the shrunk
441 // offset bore (r=3.68) that must hole this plane fell inside the
442 // enlarged void, so neither the imprint nor the orphan-rim weld
443 // could cut it.
444 // INWARD, reflex-extended: the same slide moves a REFLEX hole
445 // rim the wrong way — a sphere sitting under a frustum's base
446 // annulus (the user's 2026-09-07 document) had its r=8.3 rim
447 // scaled to 9.22 while the shrunk offset sphere (r=7.3) crosses
448 // the offset plane at r=7.23, inside the enlarged hole: the
449 // pair imprint found nothing and the rim stayed one-use.
450 // Drop the interior loops (as the wall carriers do): the
451 // neighbour's carrier imprints — or its rim, welded in as a
452 // hole — cut the true opening, and the seeded selection drops
453 // the disk over it. A CONVEX interior rim on the same face (a
454 // through-bore in a pocket floor) is safe to drop too: its
455 // offset bore imprints at r+d and the ring inside is rejected by
456 // class (Out) or by the on-skin filter, no seed involved.
457 drop_wall_interior_loops(&mut carrier)?;
458 }
459 // The rim a shadowed support's removal VACATED in this carrier's trim:
460 // a slot narrower than 2|d| that runs out through this face leaves a
461 // notch its own skin has to close, because the dilation fills the slot.
462 // The source face keeps its notch; only the carrier is bridged.
463 if distance < 0.0 && !shadowed_supports.is_empty() {
464 let report = close_vacated_rims(
465 &mut carrier,
466 source_faces
467 .iter()
468 .find(|face| face.id == *face_id)
469 .copied()
470 .unwrap_or(face),
471 &source_faces,
472 shadowed_supports,
473 )?;
474 if report.closed > 0 {
475 os_debug!(
476 "carrier for src face {face_id}: {} vacated rim run(s) bridged",
477 report.closed
478 );
479 }
480 for declined in &report.declined {
481 os_debug!("carrier for src face {face_id}: vacated rim left alone — {declined}");
482 vacated_notes.push(format!("source face {face_id}: {declined}"));
483 }
484 }
485 carriers.push(Carrier {
486 solid: carrier,
487 source_face_id: *face_id,
488 kind: OffsetFaceRole::Offset,
489 });
490 }
491 // APEX-CAP carriers: a retained face whose DEGENERATE edge (an apex
492 // point, e.g. a conic crater's tip) offsets into a RING is an EXTERIOR
493 // cone offset — the parallel surface truncates at the ring and the exact
494 // rolling-ball offset closes it with a SPHERE of radius |distance| about
495 // the apex. Emit that sphere as an extra carrier: the ring imprint
496 // (sphere × offset frustum) splits it, the on-skin filter keeps exactly
497 // the cap sector (its points are |d| from the apex and ≥|d| from the
498 // rest of the face), and the ring becomes two-use.
499 let mut apex_cap_sources = HashSet::default();
500 let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
501 {
502 let retained_count = carriers.len();
503 for index in 0..retained_count {
504 // A CARVED support's trim is the carved one. Its coedges name edges
505 // that live only in the carved clone, so the `source.edges` lookup
506 // below finds nothing and the apex-cap lane skips the face — which
507 // is right: the carve already ended that carrier at the offset's own
508 // pinch, and there is no apex ring left to cap.
509 let carved_face = carved_supports.get(&carriers[index].source_face_id);
510 let source_face = carved_face
511 .or_else(|| source_by_id_lookup(&source_faces, carriers[index].source_face_id));
512 let Some(source_face) = source_face else {
513 continue;
514 };
515 let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
516 for coedge in source_face
517 .loops
518 .iter()
519 .flat_map(|loop_record| &loop_record.coedges)
520 {
521 let Some(edge) = source
522 .edges
523 .iter()
524 .find(|edge| edge.id == coedge.edge_id)
525 else {
526 continue;
527 };
528 if !edge.degenerate {
529 continue;
530 }
531 // Image of the degenerate edge on the offset surface.
532 let [p0, p1] = coedge.pcurve.domain()?;
533 let mut ring = Vec::new();
534 for sample in 0..=8 {
535 let uv = coedge
536 .pcurve
537 .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0)?;
538 ring.push(carrier_surface.evaluate(uv.x, uv.y)?);
539 }
540 let spread = ring
541 .iter()
542 .map(|point| point.sub(ring[0]).length())
543 .fold(0.0f64, f64::max);
544 if spread <= distance.abs() * 1e-2 {
545 continue;
546 }
547 let apex = source
548 .vertices
549 .iter()
550 .find(|vertex| vertex.id == edge.start_vertex_id)
551 .map(|vertex| vertex.point)
552 .ok_or_else(|| "offset_shell: apex vertex missing".to_string())?;
553 let centroid = ring
554 .iter()
555 .fold(Vec3::default(), |sum, point| sum.add(*point))
556 .scale(1.0 / ring.len() as f64);
557 let axis = apex.sub(centroid);
558 let axis = if axis.length() > 1e-9 {
559 axis.normalized()?
560 } else {
561 Vec3::new(0.0, 0.0, 1.0)
562 };
563 os_debug!(
564 "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
565 carriers[index].source_face_id,
566 apex.x,
567 apex.y,
568 apex.z,
569 distance.abs(),
570 );
571 apex_cap_sources.insert(index);
572 apex_cap_sources.insert(carriers.len());
573 apex_cap_pairs.push((index, carriers.len()));
574 carriers.push(Carrier {
575 solid: crate::make_sphere_brep(apex, distance.abs(), axis)?,
576 source_face_id: carriers[index].source_face_id,
577 kind: OffsetFaceRole::Offset,
578 });
579 }
580 }
581 }
582 // A fold the carve's probe did not see (`outward_fold_the_carve_missed`):
583 // a band too thin for its grid is built whole, and the torus wedge grown by
584 // 1.001 then held the reflected lemon beyond its axis. Asked of every
585 // retained face the carve neither carved nor omitted, on every outward
586 // shell: a ruled wall is what first let the pipeline CLOSE such a shape,
587 // but the geometry is the trigger. Refused by name, the tangent onset
588 // included; a partial revolve of that kind is the revolved-wedge lane's
589 // question, and that lane runs in front of this pipeline.
590 if distance < 0.0 {
591 for face_id in &retained {
592 if carved_supports.contains_key(face_id)
593 || !carriers.iter().any(|carrier| carrier.source_face_id == *face_id)
594 {
595 continue;
596 }
597 let face = source_by_id_lookup(&source_faces, *face_id).unwrap();
598 if let Some(fold) = outward_fold_the_carve_missed(face, distance)? {
599 return Err(format!(
600 "offset_shell: the outward offset of source face {face_id} folds where the carve \
601 saw no fold (area factor {:.3e} at u={:.6}, v={:.6}); a fold band that thin is \
602 not built",
603 fold.factor, fold.u, fold.v
604 ));
605 }
606 }
607 }
608 // Wall carriers built as a RULED opening face extended along itself, by
609 // carrier index: the in-front guard reads their sides off the surface
610 // normal where it would read a plane's.
611 let mut ruled_walls = HashSet::default();
612 // OUTWARD through a CURVED opening. A ruled one — the cone a revolved
613 // straight profile edge sweeps, a cylinder — has its wall built as the
614 // opening face held in place and EXTENDED along itself, exactly as a plane
615 // is padded (`extended_ruled_wall`); the pad is the plane's, read off the
616 // same convex junctions. Built first, so the planar walls know which
617 // neighbours are ruled walls.
618 //
619 // Any other curved opening is refused by name. Its wall used to be the
620 // unpadded source face, which never reaches the grown offset: on a torus
621 // wedge whose chord revolves to a cone, the pair offset x cone cut 0
622 // pieces, the cone's one fragment was dropped as a void cross-section, and
623 // the result read "completion produced non-integral genus" with 12 open
624 // edges. No outward shell in the kernel suites or the case corpus closed
625 // through that wall (census, 2026-09-17).
626 let mut ruled_built = HashMap::<u64, BrepSolid>::default();
627 if distance < 0.0 {
628 for face_id in opening_face_ids {
629 let source_face = source_by_id_lookup(&source_faces, *face_id).unwrap();
630 if source_face.surface.is_affine()? {
631 continue;
632 }
633 if !ruled_opening(source_face)? {
634 return Err(format!(
635 "offset_shell: opening face {face_id} is curved and not a ruled surface; an \
636 outward wall is built only on a planar or ruled opening"
637 ));
638 }
639 let pad = outward_wall_pad(
640 source,
641 source_face,
642 &source_faces,
643 &opening_set,
644 distance.abs(),
645 )?;
646 let tolerance = 1e-7f64.max(crate::solid_scale(source) * 1e-9);
647 match extended_ruled_wall(source, source_face, pad, tolerance)? {
648 Ok(wall) => {
649 os_debug!(
650 "wall for opening face {face_id}: ruled, pad {pad:.4}, sides reach {:.4?}",
651 wall.reached
652 );
653 ruled_built.insert(*face_id, wall.solid);
654 }
655 Err(reason) => {
656 return Err(format!(
657 "offset_shell: the outward wall of ruled opening face {face_id} cannot be \
658 built: {reason}"
659 ));
660 }
661 }
662 }
663 }
664 let ruled_openings = ruled_built
665 .keys()
666 .filter_map(|id| source_by_id_lookup(&source_faces, *id))
667 .collect::<Vec<_>>();
668 for face_id in opening_face_ids {
669 let source_face = source_faces
670 .iter()
671 .find(|face| face.id == *face_id)
672 .unwrap();
673 if let Some(wall) = ruled_built.remove(face_id) {
674 ruled_walls.insert(carriers.len());
675 carriers.push(Carrier {
676 solid: wall,
677 source_face_id: *face_id,
678 kind: OffsetFaceRole::Wall,
679 });
680 continue;
681 }
682 carriers.push(Carrier {
683 solid: if distance < 0.0 && source_face.surface.is_affine()? {
684 let pad = outward_wall_pad(
685 source,
686 source_face,
687 &source_faces,
688 &opening_set,
689 distance.abs(),
690 )?;
691 // A plane meeting a RULED opening is padded past the grown
692 // outline, not onto it. Padded by exactly the pad, its trim's
693 // arc lands ON the grown offset's section wherever the join is
694 // perpendicular, and the ruled wall's section then ENDS on that
695 // coincident pair: on the torus wedge the cap shredded into 60
696 // slivers along the grown circle. The clearance is counted in
697 // the band that decides whether the strip beyond the grown
698 // outline is wall or void, so the strip always resolves as its
699 // own fragment and is dropped as void.
700 let pad = if ruled_openings
701 .iter()
702 .any(|ruled| source_faces_adjacent(source_face, ruled))
703 {
704 pad + RULED_NEIGHBOUR_CLEARANCE_BANDS
705 * offset_skin_band(crate::solid_scale(source), distance)
706 } else {
707 pad
708 };
709 os_debug!("wall for opening face {face_id}: outward pad {pad:.4}");
710 // The wall is the opening's outline OFFSET by the pad in its
711 // own plane (`outline_offset.rs`). The pad used to SCALE that
712 // outline about the patch centre, which is the offset only for
713 // a rectangle filling its patch or a circle centred on it: an
714 // L-shaped opening's notch edges pass through the centre and
715 // did not move at all, so the notch faces' grown offsets
716 // crossed the plane outside the wall ("non-integral genus",
717 // `l-step-open-bottom` at d = −1.5 in the membrane census).
718 //
719 // An outline this module does not offset keeps the scaled wall,
720 // but only when that wall still COVERS the offset outline along
721 // every edge a retained face meets — which is what the sections
722 // have to land inside. Otherwise the shell refuses by name
723 // rather than assembling an open rim.
724 let mut scaled = carrier_solid(source, *face_id, 0.0, pad)?;
725 drop_wall_interior_loops(&mut scaled)?;
726 let mut wall = match offset_outline_wall(source, source_face, pad)? {
727 Ok(outline) => {
728 // Where the scaled wall already trims the offset
729 // outline's own region — a rectangle filling its patch,
730 // a circle centred on it — it is kept, so no shell that
731 // was right before is rebuilt on a different patch.
732 let same = walls_trim_the_same_region(
733 &outline,
734 &scaled,
735 1e-9f64.max(crate::solid_scale(source) * 1e-11),
736 )?;
737 if same {
738 os_debug!("wall for opening face {face_id}: scaled, which IS its outline offset");
739 scaled
740 } else {
741 os_debug!("wall for opening face {face_id}: outline offset by {pad:.4}");
742 outline
743 }
744 }
745 Err(reason) => {
746 if let Err(uncovered) = scaled_wall_covers_offset(
747 source,
748 source_face,
749 &scaled,
750 pad,
751 &source_faces,
752 &opening_set,
753 )? {
754 return Err(format!(
755 "offset_shell: opening face {face_id}'s outline is not offset \
756 ({reason}), and its scaled outward wall does not cover the offset \
757 outline (a point of it lies {uncovered:.3e} outside)"
758 ));
759 }
760 os_debug!(
761 "wall for opening face {face_id}: scaled, which covers its offset ({reason})"
762 );
763 scaled
764 }
765 };
766 // The pad extends the wall by STRETCHING the plane's 2x2 net
767 // while the trim pcurves are cloned in UV, so every loop
768 // scales about the patch centre by (w+2|d|)/w. For the OUTER
769 // loop of a full-domain rectangle that lands exactly on the
770 // grown outline — the intended wall extent. But an INTERIOR
771 // loop (a carve outline: a bore, gouge or crater rim in the
772 // opening face) is scaled OUTWARD too, and the enlarged hole
773 // then swallows both circles the wall must be cut by (the
774 // source carve rim from the wall x source imprint and the
775 // shrunk rim from the wall x offset-carrier imprint) — the
776 // imprints clip to nothing and the rims dangle one-use. Drop
777 // interior loops instead: the wall covers the carve mouth,
778 // the imprints cut the true rims, and selection (void-wall
779 // guard + on-skin filter) drops the fragments over the void.
780 drop_wall_interior_loops(&mut wall)?;
781 wall
782 } else {
783 standalone_face(source, *face_id)?
784 },
785 source_face_id: *face_id,
786 kind: OffsetFaceRole::Wall,
787 });
788 }
789 if carriers.len() >= SOURCE_OPERAND as usize {
790 return Err("offset_shell: too many carrier faces for current ABI".into());
791 }
792 // The characteristic length every tolerance below is derived from. This
793 // was `max ‖vertex‖` — the distance from the WORLD ORIGIN — until audit
794 // slice 0: that form made every band widen as the part was moved away from
795 // the origin while ignoring the part's actual size, so the same shell
796 // solved to different tolerances depending only on where it was modelled.
797 // `solid_scale` is the bbox diagonal, floored at 1.0 exactly as the old
798 // fold was, so this changes ONE thing: placement-dependence.
799 let scale = crate::solid_scale(source);
800 crate::report_scale_migration("offset_shell", scale, || {
801 source
802 .vertices
803 .iter()
804 .map(|vertex| vertex.point.length())
805 .fold(1.0, f64::max)
806 });
807 let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
808 // Reflex-rim rebuilds run on BOTH attempts: the fallback for a broken
809 // extension is an honest refusal, but the fallback for a skipped reflex
810 // rebuild is a silently WRONG (membrane-capped) shell.
811 let reflex_rebuilt = rebuild_reflex_rim_carriers(
812 &mut carriers,
813 source,
814 &source_faces,
815 &smooth.pairs,
816 scale,
817 distance,
818 )?;
819 *reflex_rebuilds = reflex_rebuilt.len();
820 if extend_oblique_carriers {
821 let oblique_extended = extend_offset_carriers_past_open_hole_rims(
822 &mut carriers,
823 source,
824 &source_faces,
825 &opening_set,
826 &smooth.pairs,
827 scale,
828 )?;
829 let mut already_extended = oblique_extended.clone();
830 already_extended.extend(reflex_rebuilt.iter().copied());
831 let fallshort_extended = extend_fallshort_curved_carriers(
832 &mut carriers,
833 source,
834 &source_faces,
835 &opening_set,
836 &smooth.pairs,
837 &already_extended,
838 scale,
839 distance,
840 )?;
841 *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
842 os_debug!(
843 "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
844 oblique_extended.len(),
845 );
846 }
847 let tolerance = 1e-7f64.max(scale * 1e-9);
848 let pair_tolerance = (scale * 1e-8).max(2e-6);
849 let reach = distance.abs() * 4.0 + pair_tolerance;
850 let carrier_samples = carriers
851 .iter()
852 .map(|carrier| carrier_extent_points(&carrier.solid))
853 .collect::<Result<Vec<_>, _>>()?;
854 let carrier_bounds = carrier_samples
855 .iter()
856 .map(|samples| Bounds::from_points(samples))
857 .collect::<Option<Vec<_>>>()
858 .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string())?;
859 let source_by_id = source_faces
860 .iter()
861 .map(|face| (face.id, *face))
862 .collect::<HashMap<_, _>>();
863 let source_samples = source_faces
864 .iter()
865 .map(|face| {
866 let standalone = standalone_face(source, face.id)?;
867 Ok((face.id, edge_sample_points(&standalone)?))
868 })
869 .collect::<Result<HashMap<_, _>, String>>()?;
870 if debug_enabled() {
871 for (index, carrier) in carriers.iter().enumerate() {
872 let face = &carrier.solid.shells[0].faces[0];
873 let samples = &carrier_samples[index];
874 let bounds =
875 samples
876 .iter()
877 .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
878 (
879 Vec3 {
880 x: low.x.min(point.x),
881 y: low.y.min(point.y),
882 z: low.z.min(point.z),
883 },
884 Vec3 {
885 x: high.x.max(point.x),
886 y: high.y.max(point.y),
887 z: high.z.max(point.z),
888 },
889 )
890 });
891 let surface = &face.surface;
892 let rational = surface
893 .control_points
894 .iter()
895 .flatten()
896 .any(|point| (point.w - 1.0).abs() > 1e-12);
897 os_debug!(
898 "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
899 carrier.kind, carrier.source_face_id, face.loops.len(),
900 surface.degree_u, surface.degree_v,
901 surface.control_points.len(),
902 surface.control_points.first().map(|row| row.len()).unwrap_or(0),
903 bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
904 );
905 }
906 for (index, carrier) in carriers.iter().enumerate() {
907 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
908 continue;
909 }
910 let source_face = source_by_id[&carrier.source_face_id];
911 let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
912 let (ku, kv) = (
913 crate::KnotVector::new(
914 source_face.surface.knots_u.clone(),
915 source_face.surface.degree_u,
916 ),
917 crate::KnotVector::new(
918 source_face.surface.knots_v.clone(),
919 source_face.surface.degree_v,
920 ),
921 );
922 if let (Ok(ku), Ok(kv)) = (ku, kv) {
923 let [u0, u1] = ku.domain();
924 let [v0, v1] = kv.domain();
925 let mut worst = 0.0f64;
926 let mut worst_at = (0.0f64, 0.0f64);
927 for iu in 0..=24 {
928 for iv in 0..=24 {
929 let u = u0 + (u1 - u0) * iu as f64 / 24.0;
930 let v = v0 + (v1 - v0) * iv as f64 / 24.0;
931 let expected = face_offsets(source_face)
932 .at(u, v, -distance)
933 .map(|sample| sample.point);
934 let actual = carrier_surface.evaluate(u, v);
935 if let (Ok(expected), Ok(actual)) = (expected, actual) {
936 let error = expected.sub(actual).length();
937 if error > worst {
938 worst = error;
939 worst_at = (u, v);
940 }
941 }
942 }
943 }
944 let mut normal_swing = 0.0f64;
945 let mut previous: Option<Vec3> = None;
946 for iu in 0..=48 {
947 let u = u0 + (u1 - u0) * iu as f64 / 48.0;
948 let v = (v0 + v1) / 2.0;
949 if let Ok(normal) = face_normal(source_face, u, v) {
950 if let Some(previous) = previous {
951 normal_swing = normal_swing.max(previous.sub(normal).length());
952 }
953 previous = Some(normal);
954 }
955 }
956 let weights = source_face
957 .surface
958 .control_points
959 .iter()
960 .flatten()
961 .map(|point| point.w)
962 .fold((f64::MAX, f64::MIN), |(low, high), w| {
963 (low.min(w), high.max(w))
964 });
965 os_debug!(
966 "carrier[{index}] offset fit error max={worst:.6} at=({:.4},{:.4}) domain_u=({u0:.4},{u1:.4}) normal_step_max={normal_swing:.4} src_net={}x{} src_deg=({},{}) src_w=({:.4},{:.4}) knots_u={:?}",
967 worst_at.0, worst_at.1,
968 source_face.surface.control_points.len(),
969 source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
970 source_face.surface.degree_u, source_face.surface.degree_v,
971 weights.0, weights.1,
972 &source_face.surface.knots_u,
973 );
974 }
975 }
976 os_debug!("smooth pairs: {:?}", smooth.pairs);
977 }
978 let mut imprint = empty_imprint();
979 let mut next_piece_id = 1;
980 let mut next_vertex_id = 1;
981 for first in 0..carriers.len() {
982 for second in first + 1..carriers.len() {
983 let first_source = source_by_id[&carriers[first].source_face_id];
984 let second_source = source_by_id[&carriers[second].source_face_id];
985 if matches!(carriers[first].kind, OffsetFaceRole::Wall)
986 && matches!(carriers[second].kind, OffsetFaceRole::Wall)
987 {
988 // INWARD walls are the opening faces themselves: two openings
989 // never overlap and adjacent ones only touch along their
990 // shared edge, so there is nothing to imprint. OUTWARD walls
991 // are the opening planes EXTENDED by |distance|: the walls of
992 // two ADJACENT openings cross along the line of their shared
993 // source edge, and each wall ring must stop there (the sheet
994 // past that line lies in front of the other opening). Without
995 // the cut the wall × source chain below stays open on the
996 // shared-edge side (its neighbour there is an opening, not a
997 // retained face), the wall never fragments into its ring, and
998 // every source rim along both openings dangles one-use.
999 if distance > 0.0 || !source_faces_adjacent(first_source, second_source) {
1000 continue;
1001 }
1002 }
1003 if smooth.pairs.contains(&(first, second)) {
1004 os_debug!("pair ({first},{second}) skipped: smooth");
1005 continue;
1006 }
1007 if !carrier_bounds[first].intersects(carrier_bounds[second], pair_tolerance) {
1008 os_debug!("pair ({first},{second}) skipped: bounds");
1009 continue;
1010 }
1011 if !source_faces_adjacent(first_source, second_source)
1012 && sample_separation(
1013 &source_samples[&first_source.id],
1014 &source_samples[&second_source.id],
1015 ) > reach
1016 {
1017 os_debug!("pair ({first},{second}) skipped: separation");
1018 continue;
1019 }
1020 let pair = match build_imprints(
1021 &carriers[first].solid,
1022 &carriers[second].solid,
1023 &ImprintOptions {
1024 tolerance,
1025 maximum_fit_points: 96,
1026 // The GLOBAL interpolation lane. `local_fit: true` until
1027 // 2026-09-13, which cost three orders on every smooth
1028 // section: measured on the apex cone's base circle, the
1029 // local lane needs 3,070 control points to reach 2.0e-5
1030 // where the global one reaches 1.4e-8 with 384. What the
1031 // local lane is for — a rim through a corner, where its
1032 // monotonicity limiter holds a kink the global cubic rings
1033 // across, 4.9x measured — is now the fit's own fallback:
1034 // `fit_polyline` re-runs the other lane whenever the asked
1035 // one cannot meet the tolerance and returns the better
1036 // MEASURED curve, so this flag chooses which ladder is
1037 // paid for first, never which curve is delivered.
1038 local_fit: false,
1039 fit_chunk_points: None,
1040 // Offset carriers can meet in short, tightly curved
1041 // branches. The general SSI default (diagonal / 15) is
1042 // too coarse here and lets independently fitted branches
1043 // miss a shared junction when thickness crosses one of
1044 // those branches.
1045 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1046 },
1047 ) {
1048 Ok(pair) => pair,
1049 // A failed pair imprint (e.g. the marching SSI exhausting its
1050 // step budget on a long closed sphere/plane intersection) is
1051 // not fatal to the shell: the affected rim simply stays
1052 // unsplit and the rim-weld passes below get to close it. The
1053 // final watertightness gate still refuses anything the welds
1054 // cannot reach, so tolerating the miss only enlarges the
1055 // honest success set.
1056 Err(error) => {
1057 os_debug!("pair ({first},{second}) imprint failed: {error}");
1058 continue;
1059 }
1060 };
1061 let pieces_before = imprint.pieces.len();
1062 merge_pair_imprint(
1063 &mut imprint,
1064 pair,
1065 first as u8,
1066 second as u8,
1067 &mut next_piece_id,
1068 &mut next_vertex_id,
1069 tolerance,
1070 scale,
1071 );
1072 os_debug!(
1073 "pair ({first},{second}) src=({},{}) pieces+={}",
1074 carriers[first].source_face_id,
1075 carriers[second].source_face_id,
1076 imprint.pieces.len() - pieces_before,
1077 );
1078 }
1079 }
1080 if distance < 0.0 {
1081 // OUTWARD shells: the opening-wall carrier is the opening face's
1082 // plane EXTENDED by |distance|, so its trim carries only the GROWN
1083 // outline. Its inner boundary — where the wall ring stops at the
1084 // source solid — is the source outline, and the retained SOURCE faces
1085 // are not carriers, so no carrier×carrier pair ever imprints it.
1086 // Un-cut, the wall stays one whole-plane fragment covering the
1087 // opening void, and the source faces' rim edges dangle one-use.
1088 // Imprint each wall carrier against the retained source faces that
1089 // share an edge with its opening: the wall then fragments into the
1090 // ring (kept) and the opening hole (dropped by the void guard).
1091 for index in 0..carriers.len() {
1092 if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
1093 continue;
1094 }
1095 let opening = source_by_id[&carriers[index].source_face_id];
1096 for retained_id in &retained {
1097 let retained_face = source_by_id[retained_id];
1098 if !source_faces_adjacent(opening, retained_face) {
1099 continue;
1100 }
1101 let neighbor = standalone_face(source, *retained_id)?;
1102 let pair = match build_imprints(
1103 &carriers[index].solid,
1104 &neighbor,
1105 &ImprintOptions {
1106 tolerance,
1107 maximum_fit_points: 96,
1108 // The global lane, as above.
1109 local_fit: false,
1110 fit_chunk_points: None,
1111 maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
1112 },
1113 ) {
1114 Ok(pair) => pair,
1115 Err(error) => {
1116 os_debug!(
1117 "wall×source pair ({index},{retained_id}) imprint failed: {error}"
1118 );
1119 continue;
1120 }
1121 };
1122 let pieces_before = imprint.pieces.len();
1123 merge_pair_imprint(
1124 &mut imprint,
1125 pair,
1126 index as u8,
1127 SOURCE_OPERAND,
1128 &mut next_piece_id,
1129 &mut next_vertex_id,
1130 tolerance,
1131 scale,
1132 );
1133 os_debug!(
1134 "wall×source pair ({index},{retained_id}) pieces+={}",
1135 imprint.pieces.len() - pieces_before,
1136 );
1137 }
1138 }
1139 }
1140 // APEX-CAP ring splits: the cap × cone section lies ON the cone
1141 // carrier's promoted ring edge (an overlap, not a crossing), so
1142 // `build_imprints` mints no edge splits for the cone side. Without them
1143 // the cone keeps ONE closed ring edge while the cap carries the section
1144 // as arcs — mismatched segmentation, both sides one-use. Record the arc
1145 // junctions as splits on the cone's coincident edge.
1146 for (cone_index, cap_index) in &apex_cap_pairs {
1147 let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
1148 let piece_ids = imprint
1149 .by_face
1150 .iter()
1151 .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
1152 .map(|entry| entry.piece_ids.clone())
1153 .unwrap_or_default();
1154 let coincidence_band = 2e-5f64.max(scale * 1e-7);
1155 for piece_id in piece_ids {
1156 let Some((curve, t0, t1)) = imprint
1157 .pieces
1158 .iter()
1159 .find(|piece| piece.id == piece_id)
1160 .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
1161 else {
1162 continue;
1163 };
1164 for endpoint in [curve.evaluate(t0)?, curve.evaluate(t1)?] {
1165 for edge in &carriers[*cone_index].solid.edges {
1166 if edge.degenerate {
1167 continue;
1168 }
1169 let projection = crate::project_point_to_curve(&edge.curve, endpoint)?;
1170 if projection.distance > coincidence_band
1171 || projection.u < edge.t0 + 1e-9
1172 || projection.u > edge.t1 - 1e-9
1173 {
1174 continue;
1175 }
1176 let parameter = projection.u;
1177 if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
1178 split.operand == *cone_index as u8 && split.edge_id == edge.id
1179 }) {
1180 if !existing
1181 .parameters
1182 .iter()
1183 .any(|value| (*value - parameter).abs() <= 1e-8)
1184 {
1185 existing.parameters.push(parameter);
1186 }
1187 } else {
1188 imprint.edge_splits.push(EdgeSplitRecord {
1189 operand: *cone_index as u8,
1190 edge_id: edge.id,
1191 parameters: vec![parameter],
1192 });
1193 }
1194 os_debug!(
1195 "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
1196 edge.id,
1197 );
1198 }
1199 }
1200 }
1201 }
1202 if debug_enabled() {
1203 for piece in &imprint.pieces {
1204 let start = piece.curve.evaluate(piece.t0);
1205 let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
1206 let end = piece.curve.evaluate(piece.t1);
1207 if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
1208 os_debug!(
1209 "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1210 piece.id,
1211 start.x,
1212 start.y,
1213 start.z,
1214 mid.x,
1215 mid.y,
1216 mid.z,
1217 end.x,
1218 end.y,
1219 end.z,
1220 );
1221 }
1222 }
1223 }
1224 synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
1225 // This decides whether an imprint is genuinely the carrier's existing
1226 // trim, not whether independently fitted curves can later be sewn. The
1227 // looser assembly tolerance can erase a short separating branch while it
1228 // is still departing a boundary.
1229 let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
1230 let piece_geometry = imprint
1231 .pieces
1232 .iter()
1233 .map(|piece| {
1234 (
1235 piece.id,
1236 FragmentEdgeGeometry {
1237 curve: piece.curve.clone(),
1238 t0: piece.t0,
1239 t1: piece.t1,
1240 },
1241 )
1242 })
1243 .collect::<HashMap<_, _>>();
1244 for (index, carrier) in carriers.iter().enumerate() {
1245 if !matches!(carrier.kind, OffsetFaceRole::Offset) {
1246 continue;
1247 }
1248 let boundaries = carrier
1249 .solid
1250 .edges
1251 .iter()
1252 .map(|edge| FragmentEdgeGeometry {
1253 curve: edge.curve.clone(),
1254 t0: edge.t0,
1255 t1: edge.t1,
1256 })
1257 .collect::<Vec<_>>();
1258 if let Some(by_face) = imprint
1259 .by_face
1260 .iter_mut()
1261 .find(|entry| entry.operand == index as u8)
1262 {
1263 let before = by_face.piece_ids.clone();
1264 by_face.piece_ids.retain(|piece_id| {
1265 let piece = &piece_geometry[piece_id];
1266 !boundaries.iter().any(|boundary| {
1267 fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
1268 .unwrap_or(false)
1269 })
1270 });
1271 if debug_enabled() && before.len() != by_face.piece_ids.len() {
1272 os_debug!(
1273 "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
1274 before,
1275 by_face.piece_ids,
1276 );
1277 }
1278 }
1279 }
1280
1281 if debug_enabled() {
1282 for entry in &imprint.by_face {
1283 os_debug!(
1284 "by_face operand={} face={} pieces={:?}",
1285 entry.operand,
1286 entry.face_id,
1287 entry.piece_ids,
1288 );
1289 }
1290 }
1291 let mut split_carriers = Vec::new();
1292 let mut fragments_by_carrier = Vec::new();
1293 for (index, carrier) in carriers.iter().enumerate() {
1294 // Offset carriers keep their boundary un-split (their trims never
1295 // coincide with imprints — the boundary-coincidence filter drops such
1296 // pieces instead). An apex-cap ring breaks that assumption: the
1297 // frustum's ring is its TRIM boundary while the cap crosses it as TWO
1298 // imprint arcs — without splitting, the two sides segment differently
1299 // and both stay one-use. Split exactly the cap-affected carriers.
1300 let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
1301 && !apex_cap_sources.contains(&index)
1302 {
1303 carrier.solid.clone()
1304 } else {
1305 apply_edge_splits(&carrier.solid, index as u8, &imprint)?
1306 };
1307 let fragments = fragment_solid(&split, index as u8, &imprint)?;
1308 os_debug!(
1309 "carrier[{index}] kind={:?} src={} fragments={}",
1310 carrier.kind,
1311 carrier.source_face_id,
1312 fragments.len(),
1313 );
1314 if debug_enabled() {
1315 for (fragment_index, fragment) in fragments.iter().enumerate() {
1316 let mut boundary_count = 0usize;
1317 let mut imprint_pieces = Vec::new();
1318 let mut derived_count = 0usize;
1319 for coedge in fragment
1320 .loops
1321 .iter()
1322 .flat_map(|loop_record| &loop_record.coedges)
1323 {
1324 match &coedge.source {
1325 FragmentEdgeSource::Boundary { .. }
1326 | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
1327 FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
1328 FragmentEdgeSource::Derived { .. } => derived_count += 1,
1329 }
1330 }
1331 os_debug!(
1332 " frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
1333 fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
1334 fragment.test_uv.x, fragment.test_uv.y,
1335 );
1336 }
1337 }
1338 split_carriers.push(split);
1339 fragments_by_carrier.push(fragments);
1340 }
1341 let mut assembly_sources = [(SOURCE_OPERAND, source)]
1342 .into_iter()
1343 .collect::<HashMap<_, _>>();
1344 for (index, carrier) in split_carriers.iter().enumerate() {
1345 assembly_sources.insert(index as u8, carrier);
1346 }
1347
1348 let source_edge_by_id = source
1349 .edges
1350 .iter()
1351 .map(|edge| (edge.id, edge))
1352 .collect::<HashMap<_, _>>();
1353 let retained_faces_with_edges = retained
1354 .iter()
1355 .map(|face_id| {
1356 let face = source_by_id[face_id];
1357 let edges = face
1358 .loops
1359 .iter()
1360 .flat_map(|loop_record| &loop_record.coedges)
1361 .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
1362 .collect::<Vec<_>>();
1363 (face, edges)
1364 })
1365 .collect::<Vec<_>>();
1366 let offset_skin_tolerance = offset_skin_band(scale, distance);
1367 // OUTWARD shells: a wall fragment lying IN FRONT of another opening —
1368 // past that opening's plane on its void side, within the extent of its
1369 // (extended) wall carrier — is not material: an opening removes
1370 // everything in front of it. Such a fragment is the sheet of this wall
1371 // between the source outline and the grown outline on the far side of an
1372 // ADJACENT opening's shared edge (the x∈[−1,0] strip of an opened −Z
1373 // face's wall, in front of the opened −X face). It sits within |distance|
1374 // of a retained face's corner edge, so the void-cross-section guard reads
1375 // it as wall thickness; only the other opening's plane tells it apart.
1376 //
1377 // A RULED opening's wall is curved, so it has no one plane: its front is
1378 // read at the point's foot on the wall carrier, along the carrier's normal
1379 // turned to the opening face's outward side. On the torus wedge that is
1380 // what drops the start and end caps' strips beyond the extended chord,
1381 // which lie within |distance| of the torus and in front of the cone.
1382 let mut opening_planes: Vec<(usize, Vec3, Vec3)> = Vec::new();
1383 let mut opening_ruled: Vec<(usize, f64)> = Vec::new();
1384 if distance < 0.0 {
1385 for (index, carrier) in carriers.iter().enumerate() {
1386 if !matches!(carrier.kind, OffsetFaceRole::Wall) {
1387 continue;
1388 }
1389 let opening = source_by_id[&carrier.source_face_id];
1390 let [u0, u1] = opening.surface.domain_u()?;
1391 let [v0, v1] = opening.surface.domain_v()?;
1392 let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
1393 if ruled_walls.contains(&index) {
1394 let wall = &split_carriers[index].shells[0].faces[0];
1395 let probe = project_point_to_surface(&wall.surface, opening.surface.evaluate(u, v)?)?;
1396 let side = face_normal(wall, probe.u, probe.v)?.dot(face_normal(opening, u, v)?);
1397 opening_ruled.push((index, if side < 0.0 { -1.0 } else { 1.0 }));
1398 continue;
1399 }
1400 if !opening.surface.is_affine()? {
1401 continue;
1402 }
1403 opening_planes.push((
1404 index,
1405 opening.surface.evaluate(u, v)?,
1406 face_normal(opening, u, v)?,
1407 ));
1408 }
1409 }
1410 let in_front_of_other_opening = |wall_index: usize, point: Vec3| -> Result<bool, String> {
1411 let band = 2e-3f64.max(scale * 5e-5);
1412 for (index, plane_point, outward) in &opening_planes {
1413 if *index == wall_index || point.sub(*plane_point).dot(*outward) <= band {
1414 continue;
1415 }
1416 let face = &split_carriers[*index].shells[0].faces[0];
1417 let projection = project_point_to_surface(&face.surface, point)?;
1418 let uv = Vec2 {
1419 x: projection.u,
1420 y: projection.v,
1421 };
1422 if parameter_point_in_face(face, uv, 1e-8)? != PolygonClass::Outside {
1423 return Ok(true);
1424 }
1425 }
1426 for (index, side) in &opening_ruled {
1427 if *index == wall_index {
1428 continue;
1429 }
1430 let face = &split_carriers[*index].shells[0].faces[0];
1431 let projection = project_point_to_surface(&face.surface, point)?;
1432 let outward = face_normal(face, projection.u, projection.v)?.scale(*side);
1433 if point.sub(projection.point).dot(outward) <= band {
1434 continue;
1435 }
1436 let uv = Vec2 {
1437 x: projection.u,
1438 y: projection.v,
1439 };
1440 if parameter_point_in_face(face, uv, 1e-8)? != PolygonClass::Outside {
1441 return Ok(true);
1442 }
1443 }
1444 Ok(false)
1445 };
1446
1447 let mut chosen_offsets = Vec::new();
1448 let mut chosen_walls = Vec::new();
1449 let mut newly_shadowed = HashSet::<u64>::default();
1450 for (index, carrier) in carriers.iter().enumerate() {
1451 let raw_seeds = source_seeds(source, carrier.source_face_id)?;
1452 let mut seeds = Vec::with_capacity(raw_seeds.len());
1453 for raw_seed in raw_seeds {
1454 let seed = if matches!(carrier.kind, OffsetFaceRole::Offset) {
1455 offset_seed(
1456 source_by_id[&carrier.source_face_id],
1457 raw_seed,
1458 &split_carriers[index].shells[0].faces[0].surface,
1459 distance,
1460 2e-3f64.max(scale * 5e-5),
1461 )?
1462 .unwrap_or(raw_seed)
1463 } else {
1464 raw_seed
1465 };
1466 if seed.sub(raw_seed).length() > 1e-9 {
1467 os_debug!(
1468 " seed[{index}] moved from ({:.4},{:.4}) to the offset image ({:.4},{:.4})",
1469 raw_seed.x,
1470 raw_seed.y,
1471 seed.x,
1472 seed.y
1473 );
1474 }
1475 seeds.push(seed);
1476 }
1477 let seed = seeds[0];
1478 if matches!(carrier.kind, OffsetFaceRole::Offset) {
1479 // Fragments whose test point sits at the full offset distance
1480 // from every retained source face are the true offset-skin
1481 // regions. Fragments closer to some other face are shadowed
1482 // pockets/strips that another carrier owns.
1483 let readings = fragments_by_carrier[index]
1484 .iter()
1485 .map(|fragment| {
1486 offset_skin_reading(
1487 fragment.test_point,
1488 &retained_faces_with_edges,
1489 distance,
1490 offset_skin_tolerance,
1491 )
1492 })
1493 .collect::<Vec<_>>();
1494 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1495 if let SkinReading::Unmeasured { face, reason } = reading {
1496 return Err(unmeasured_skin_refusal(fragment.test_point, *face, reason));
1497 }
1498 }
1499 // EVERY fragment measured nearer than the distance to some other
1500 // retained face: this support's offset is SHADOWED over its whole
1501 // extended carrier — the side walls of a slot narrower than 2|d|
1502 // grown outward, a pocket floor within 2d of the face under it.
1503 // It owns no skin. It used to switch the filter off and keep its
1504 // seeded fragment anyway, which built a closed, validating shell
1505 // with an inverted box inside it (+90 on a width-2 slot grown by
1506 // 1.5, with the 90 of material at its centre missing). Dropping
1507 // only its fragments is not enough: its carrier has already cut
1508 // its neighbours, and the top skin over that slot keeps a hole
1509 // nothing fills. So the pass is re-run with the support omitted
1510 // before the imprint, like a collapsed one. An apex-cap sphere
1511 // shares its cone's source id and is not judged here.
1512 //
1513 // Only the FIRST pass omits. A test point speaks for its fragment
1514 // because every other carrier's imprint bounds it; omitting a
1515 // carrier removes cuts, merges fragments and moves test points,
1516 // but changes no distance, so a support that reads shadowed only
1517 // AFTER an omission is reading a test point that no longer stands
1518 // for its fragment. Measured on two bosses 1 apart grown outward
1519 // by 1.5: once the facing boss walls were omitted, the plate's
1520 // grown top — one uncut fragment, mostly skin — read shadowed at
1521 // its single test point.
1522 //
1523 // "Every fragment" counts a fragment the selection would drop by
1524 // CLASS as not skin, as the selection does: the extended carrier's
1525 // surplus past a slot's end reads at least the distance from every
1526 // face, lies inside the source, and on a slot 1 wide grown outward
1527 // left the slot floor unjudged until the side walls were gone.
1528 // ADRIFT: a fragment whose test point is farther from its OWN face
1529 // than the distance, in the mitered metric, is not that face's skin
1530 // — it is inside the cavity, past where its neighbours' offsets cut
1531 // it. A rib, a pin or a divider thinner than 2d shelled inward
1532 // keeps such fragments, and the assembly then fails on them.
1533 let adrift = fragments_by_carrier[index]
1534 .iter()
1535 .map(|fragment| {
1536 point_adrift_of_its_own_offset(
1537 fragment.test_point,
1538 carrier.source_face_id,
1539 &retained_faces_with_edges,
1540 distance,
1541 offset_skin_tolerance,
1542 )
1543 })
1544 .collect::<Vec<_>>();
1545 let mut skinless = readings
1546 .iter()
1547 .any(|reading| matches!(reading, SkinReading::Shadowed { .. }))
1548 || adrift.iter().any(|flag| *flag);
1549 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1550 if !skinless {
1551 break;
1552 }
1553 if matches!(reading, SkinReading::On)
1554 && !adrift[fragments_by_carrier[index]
1555 .iter()
1556 .position(|other| std::ptr::eq(other, fragment))
1557 .unwrap_or(0)]
1558 {
1559 let class = classify_point(fragment.test_point, source, tolerance * 10.0)?.class;
1560 skinless = (distance > 0.0 && class == PointClass::Out)
1561 || (distance < 0.0 && class == PointClass::In);
1562 }
1563 }
1564 if skinless && !apex_cap_sources.contains(&index) {
1565 if !shadowed_supports.is_empty() {
1566 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
1567 omitted.sort_unstable();
1568 return Err(format!(
1569 "offset_shell: omitting the shadowed offsets of source faces {omitted:?} \
1570 leaves the offset of source face {} shadowed at every one of its {} \
1571 fragment test points, which no longer stand for their fragments once \
1572 those carriers stop cutting it",
1573 carrier.source_face_id,
1574 readings.len()
1575 ));
1576 }
1577 os_debug!(
1578 " select[{index}] src={} SHADOWED over all {} fragment(s): {:?}",
1579 carrier.source_face_id,
1580 readings.len(),
1581 readings
1582 );
1583 newly_shadowed.insert(carrier.source_face_id);
1584 continue;
1585 }
1586 // Partly shadowed: keep a bounded sample of the shadowed test
1587 // points, so a refusal can say where two offsets cross.
1588 for (fragment, reading) in fragments_by_carrier[index].iter().zip(&readings) {
1589 if shadowed_points.len() >= MAXIMUM_CROSSING_READS {
1590 break;
1591 }
1592 if matches!(reading, SkinReading::Shadowed { .. }) {
1593 shadowed_points.push((fragment.test_point, carrier.source_face_id));
1594 }
1595 }
1596 let on_skin = readings
1597 .iter()
1598 .zip(&adrift)
1599 .map(|(reading, adrift)| matches!(reading, SkinReading::On) && !adrift)
1600 .collect::<Vec<_>>();
1601 let skin_filter_active = on_skin.iter().any(|flag| *flag);
1602 let mut classified = Vec::new();
1603 let mut viable = Vec::new();
1604 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
1605 let class = classify_point(fragment.test_point, source, tolerance * 10.0)?.class;
1606 let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
1607 || (distance < 0.0 && class == PointClass::In);
1608 let contact = if excluded_by_class {
1609 false
1610 } else {
1611 distance > 0.0
1612 && fragment_has_sustained_source_contact(
1613 fragment,
1614 &source_faces,
1615 &assembly_sources,
1616 &imprint,
1617 2e-3f64.max(scale * 5e-5),
1618 )?
1619 };
1620 os_debug!(
1621 " select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
1622 on_skin[fragment_index],
1623 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
1624 fragment.test_uv.x, fragment.test_uv.y,
1625 );
1626 if excluded_by_class || (skin_filter_active && !on_skin[fragment_index]) {
1627 continue;
1628 }
1629 classified.push(fragment.clone());
1630 if contact {
1631 continue;
1632 }
1633 viable.push(fragment.clone());
1634 }
1635 // One skin per REGION of the source face (see `source_seeds`):
1636 // every region of a pinched face is offset like a face of its
1637 // own, and the selection below answers each region's seed. An
1638 // ordinary face has the one seed and the one answer it always had.
1639 let mut region_skins = Vec::new();
1640 for &seed in &seeds {
1641 os_debug!(" select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
1642 let seeded = viable
1643 .iter()
1644 .filter(|fragment| {
1645 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
1646 .is_ok_and(|class| class != PolygonClass::Outside)
1647 })
1648 .cloned()
1649 .collect::<Vec<_>>();
1650 let seeded_classified = classified
1651 .iter()
1652 .filter(|fragment| {
1653 parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
1654 .is_ok_and(|class| class != PolygonClass::Outside)
1655 })
1656 .cloned()
1657 .collect::<Vec<_>>();
1658 let selected = if viable.is_empty() {
1659 if !skin_filter_active {
1660 // NOTHING on this carrier reads on-skin: every classified
1661 // fragment's test point is measurably CLOSER than the
1662 // offset distance to some retained face — a buried strip
1663 // another carrier owns. Forcing the largest one in anyway
1664 // plants a wrong skin patch that collides with the
1665 // completion phase's reconstruction (3-use edges).
1666 // Contribute nothing: `complete_opening_boundary_cycles`
1667 // rebuilds the true skin from the carrier surface, and the
1668 // watertight gate still refuses if it cannot.
1669 None
1670 } else {
1671 let mut by_area = classified
1672 .iter()
1673 .cloned()
1674 .map(|fragment| {
1675 let area = parameter_space_area(&fragment_as_trim(&fragment))?.abs();
1676 Ok((fragment, area))
1677 })
1678 .collect::<Result<Vec<_>, String>>()?;
1679 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
1680 by_area.into_iter().next().map(|entry| entry.0)
1681 }
1682 } else if !seeded_classified.is_empty() {
1683 let mut candidates = seeded_classified;
1684 candidates.sort_by(|a, b| {
1685 a.test_uv
1686 .sub(seed)
1687 .length()
1688 .total_cmp(&b.test_uv.sub(seed).length())
1689 });
1690 candidates.into_iter().next()
1691 } else if viable.len() > 2 {
1692 let mut by_area = viable
1693 .iter()
1694 .cloned()
1695 .map(|fragment| {
1696 let area = parameter_space_area(&fragment_as_trim(&fragment))?.abs();
1697 Ok((fragment, area))
1698 })
1699 .collect::<Result<Vec<_>, String>>()?;
1700 by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
1701 by_area.into_iter().next().map(|entry| entry.0)
1702 } else {
1703 let mut candidates = if seeded.is_empty() { viable.clone() } else { seeded };
1704 candidates.sort_by(|a, b| {
1705 a.test_uv
1706 .sub(seed)
1707 .length()
1708 .total_cmp(&b.test_uv.sub(seed).length())
1709 });
1710 candidates.into_iter().next()
1711 };
1712 if let Some(fragment) = &selected {
1713 os_debug!(
1714 " select[{index}] src={} chosen test point departs its own face's offset \
1715 distance by {:?}",
1716 carrier.source_face_id,
1717 own_offset_departure(
1718 fragment.test_point,
1719 carrier.source_face_id,
1720 &retained_faces_with_edges,
1721 distance
1722 )
1723 );
1724 os_debug!(
1725 " select[{index}] src={} chosen test point departs in the MITER metric by {:?}",
1726 carrier.source_face_id,
1727 own_miter_departure(
1728 fragment.test_point,
1729 carrier.source_face_id,
1730 &retained_faces_with_edges,
1731 distance
1732 )
1733 );
1734 if let Some((other, separation)) = coincident_offset_face(
1735 fragment.test_point,
1736 carrier.source_face_id,
1737 &retained_faces_with_edges,
1738 distance,
1739 offset_skin_tolerance,
1740 )? {
1741 return Err(format!(
1742 "offset_shell: the offsets of source faces {} and {other} coincide near \
1743 ({:.4}, {:.4}, {:.4}) — the material between those faces is twice the \
1744 shell distance thick (separation {separation:.6} against {:.6}, band \
1745 {offset_skin_tolerance:.1e}), so the cavity between them has no \
1746 thickness there; a cavity pinched to zero thickness is not supported",
1747 carrier.source_face_id,
1748 fragment.test_point.x,
1749 fragment.test_point.y,
1750 fragment.test_point.z,
1751 distance.abs(),
1752 ));
1753 }
1754 }
1755 if let Some(fragment) = selected {
1756 // A pinched face's seeds each find their own region; a
1757 // fallback that ignores the seed can answer the same
1758 // fragment twice, and one skin must enter once.
1759 if !region_skins
1760 .iter()
1761 .any(|other: &FaceFragmentRecord| {
1762 other.test_point.sub(fragment.test_point).length() <= 1e-12
1763 })
1764 {
1765 region_skins.push(fragment);
1766 }
1767 }
1768 }
1769 chosen_offsets.extend(region_skins);
1770 } else {
1771 // Keep arrangement order stable. Besides making the result
1772 // reproducible, this matches the reference kernel's Set insertion
1773 // order when the final manifold-wall guard admits fragments.
1774 let opening = source_by_id[&carrier.source_face_id];
1775 let wall_seeds = opening_wall_seeds(
1776 opening,
1777 &split_carriers[index].shells[0].faces[0],
1778 source,
1779 &opening_set,
1780 distance,
1781 )?;
1782 // The wall-seed exemptions below rest on "the coplanar-rim weld
1783 // cuts the void hole later" — which only ever happens on PLANAR
1784 // opening carriers. A curved carrier (e.g. the sphere face used
1785 // directly as the opening) keeps its un-split fragment forever:
1786 // exempting it would seal the opening with the source face itself
1787 // and fabricate a closed hollow solid. Curved carriers therefore
1788 // get the strict void guard.
1789 let carrier_planar =
1790 surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
1791 .unwrap_or(false);
1792 let mut selected_indices = Vec::new();
1793 for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
1794 if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)?
1795 == PolygonClass::Outside
1796 {
1797 if in_front_of_other_opening(index, fragment.test_point)? {
1798 os_debug!(
1799 " wall[{index}.{fragment_index}] skipped: in front of another \
1800 opening at ({:.3},{:.3},{:.3})",
1801 fragment.test_point.x,
1802 fragment.test_point.y,
1803 fragment.test_point.z,
1804 );
1805 continue;
1806 }
1807 // Wall = the material cross-section exposed at the
1808 // opening: every point of a genuine wall fragment lies
1809 // WITHIN the offset distance of some retained face (it is
1810 // the thickness between that face and its offset skin). A
1811 // fragment at ≥ distance from EVERY retained face — and
1812 // containing no expected wall seed (an un-split cap
1813 // fragment can span both the void AND the wall annulus;
1814 // the coplanar-rim weld cuts its hole later) — is the
1815 // core's own cross-section: a HOLE in the opening (e.g.
1816 // the corner void where a carve's offset circle passes
1817 // just inside the opening's corner). Welding it shut
1818 // would fabricate a wall over the void.
1819 if point_on_offset_skin(
1820 fragment.test_point,
1821 &retained_faces_with_edges,
1822 distance,
1823 offset_skin_tolerance,
1824 )? && !(carrier_planar
1825 && wall_seeds.iter().any(|wall_seed| {
1826 parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
1827 .is_ok_and(|class| class != PolygonClass::Outside)
1828 }))
1829 {
1830 os_debug!(
1831 " wall[{index}.{fragment_index}] skipped: void cross-section at \
1832 ({:.3},{:.3},{:.3})",
1833 fragment.test_point.x,
1834 fragment.test_point.y,
1835 fragment.test_point.z,
1836 );
1837 continue;
1838 }
1839 selected_indices.push(fragment_index);
1840 }
1841 }
1842 for wall_seed in wall_seeds {
1843 let fragments = &fragments_by_carrier[index];
1844 let nearest_containing = fragments
1845 .iter()
1846 .enumerate()
1847 .filter(|(_, fragment)| {
1848 parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
1849 .is_ok_and(|class| class != PolygonClass::Outside)
1850 })
1851 .min_by(|(_, first), (_, second)| {
1852 first
1853 .test_uv
1854 .sub(wall_seed)
1855 .length()
1856 .total_cmp(&second.test_uv.sub(wall_seed).length())
1857 })
1858 .map(|(fragment_index, _)| fragment_index);
1859 // Arrangement fitting can leave the seed just outside every
1860 // fragment by a small amount. Match the reference kernel's
1861 // geometric fallback instead of silently omitting the wall
1862 // fragment and leaving the assembled shell open.
1863 let nearest = if nearest_containing.is_some() {
1864 nearest_containing
1865 } else {
1866 let seed_point = split_carriers[index].shells[0].faces[0]
1867 .surface
1868 .evaluate(wall_seed.x, wall_seed.y)?;
1869 fragments
1870 .iter()
1871 .enumerate()
1872 .min_by(|(_, first), (_, second)| {
1873 first
1874 .test_point
1875 .sub(seed_point)
1876 .length()
1877 .total_cmp(&second.test_point.sub(seed_point).length())
1878 })
1879 .map(|(fragment_index, _)| fragment_index)
1880 };
1881 if let Some(fragment_index) = nearest {
1882 let candidate = &fragments_by_carrier[index][fragment_index];
1883 if in_front_of_other_opening(index, candidate.test_point)? {
1884 continue;
1885 }
1886 // Void-cross-section guard. On a PLANAR carrier it applies
1887 // to the GEOMETRIC FALLBACK only: a fragment that
1888 // genuinely contains this wall seed is wall content by
1889 // construction (even when un-split it also spans void —
1890 // the rim weld cuts the hole later), but a merely-nearest
1891 // fragment must not resurrect a hole in the opening. On a
1892 // CURVED carrier no later weld cuts the hole, so even a
1893 // seed-containing fragment gets the strict guard.
1894 if (nearest_containing.is_none() || !carrier_planar)
1895 && point_on_offset_skin(
1896 candidate.test_point,
1897 &retained_faces_with_edges,
1898 distance,
1899 offset_skin_tolerance,
1900 )?
1901 {
1902 os_debug!(
1903 " wall[{index}.{fragment_index}] seed-hit skipped: void \
1904 cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
1905 candidate.test_point.x,
1906 candidate.test_point.y,
1907 candidate.test_point.z,
1908 );
1909 continue;
1910 }
1911 let existing = chosen_offsets.iter().cloned().chain(
1912 selected_indices
1913 .iter()
1914 .map(|selected| fragments_by_carrier[index][*selected].clone()),
1915 );
1916 if !selected_indices.contains(&fragment_index)
1917 && !would_overuse_existing_boundary(
1918 candidate,
1919 existing,
1920 &assembly_sources,
1921 &imprint,
1922 2e-3,
1923 )?
1924 {
1925 selected_indices.push(fragment_index);
1926 }
1927 }
1928 }
1929 for fragment_index in selected_indices.iter().copied() {
1930 chosen_walls.push(fragments_by_carrier[index][fragment_index].clone());
1931 }
1932 }
1933 }
1934 if debug_enabled() {
1935 for fragment in &chosen_offsets {
1936 os_debug!(
1937 "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
1938 fragment.operand,
1939 carriers[fragment.operand as usize].source_face_id,
1940 fragment.test_point.x,
1941 fragment.test_point.y,
1942 fragment.test_point.z,
1943 );
1944 for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
1945 for coedge in &loop_record.coedges {
1946 let surface = &fragment.surface;
1947 let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
1948 let uv0 = coedge.pcurve.evaluate(c0);
1949 let uv1 = coedge.pcurve.evaluate(c1);
1950 if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
1951 let p0 = surface.evaluate(uv0.x, uv0.y);
1952 let p1 = surface.evaluate(uv1.x, uv1.y);
1953 if let (Ok(p0), Ok(p1)) = (p0, p1) {
1954 let kind = match &coedge.source {
1955 FragmentEdgeSource::Boundary { edge_id, .. } => {
1956 format!("bnd:{edge_id}")
1957 }
1958 FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
1959 format!("sbnd:{edge_id}")
1960 }
1961 FragmentEdgeSource::Imprint { piece_id } => {
1962 format!("imp:{piece_id}")
1963 }
1964 FragmentEdgeSource::Derived { .. } => "derived".to_string(),
1965 };
1966 os_debug!(
1967 " loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1968 p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
1969 );
1970 }
1971 }
1972 }
1973 }
1974 }
1975 for fragment in &chosen_walls {
1976 os_debug!(
1977 "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
1978 fragment.operand,
1979 carriers[fragment.operand as usize].source_face_id,
1980 fragment.test_point.x,
1981 fragment.test_point.y,
1982 fragment.test_point.z,
1983 );
1984 }
1985 }
1986 if !newly_shadowed.is_empty() {
1987 let mut omitted = shadowed_supports.clone();
1988 omitted.extend(newly_shadowed.iter().copied());
1989 os_debug!(
1990 "re-running without shadowed offset supports {:?} (omitted before: {:?})",
1991 newly_shadowed,
1992 shadowed_supports
1993 );
1994 // What this pass sampled belongs to carriers the next pass may not
1995 // build.
1996 shadowed_points.clear();
1997 return offset_shell_impl(
1998 source,
1999 opening_face_ids,
2000 distance,
2001 extend_oblique_carriers,
2002 extension_fired,
2003 reflex_rebuilds,
2004 &omitted,
2005 shadowed_points,
2006 omitted_supports,
2007 vacated_notes,
2008 );
2009 }
2010 if chosen_offsets.is_empty() && !shadowed_supports.is_empty() {
2011 let mut omitted = shadowed_supports.iter().copied().collect::<Vec<_>>();
2012 omitted.sort_unstable();
2013 return Err(format!(
2014 "offset_shell: no retained face keeps any offset skin once the shadowed offsets of \
2015 source faces {omitted:?} are omitted — the solid is nowhere thicker than twice the \
2016 shell distance, so there is no cavity to shell"
2017 ));
2018 }
2019 if chosen_offsets.is_empty() {
2020 return Err("offset_shell: carrier intersections produced no offset boundary".into());
2021 }
2022 // Opening-wall fragments can overlap at dense carrier junctions. The
2023 // seed-time guard above handles geometric containment; this final pass
2024 // mirrors the topology-level rule and counts exact fragment edge sources.
2025 let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
2026 for fragment in &chosen_offsets {
2027 for coedge in fragment
2028 .loops
2029 .iter()
2030 .flat_map(|loop_record| &loop_record.coedges)
2031 {
2032 let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
2033 *boundary_use_count.entry(key).or_default() += 1;
2034 }
2035 }
2036 let mut manifold_walls = Vec::new();
2037 for fragment in chosen_walls {
2038 let keys = fragment
2039 .loops
2040 .iter()
2041 .flat_map(|loop_record| &loop_record.coedges)
2042 .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
2043 .collect::<Result<Vec<_>, _>>()?;
2044 if !keys
2045 .iter()
2046 .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
2047 {
2048 for key in keys {
2049 *boundary_use_count.entry(key).or_default() += 1;
2050 }
2051 manifold_walls.push(fragment);
2052 }
2053 }
2054 os_debug!(
2055 "manifold walls kept: {} operands={:?}",
2056 manifold_walls.len(),
2057 manifold_walls
2058 .iter()
2059 .map(|fragment| fragment.operand)
2060 .collect::<Vec<_>>(),
2061 );
2062 let chosen_walls = manifold_walls;
2063
2064 let source_fragments = fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?;
2065 let mut selected = source_fragments
2066 .into_iter()
2067 .filter(|fragment| retained.contains(&fragment.source_face_id))
2068 .collect::<Vec<_>>();
2069 let mut face_images = selected
2070 .iter()
2071 .map(|fragment| OffsetShellFaceImageRecord {
2072 role: OffsetFaceRole::Source,
2073 source_face_id: fragment.source_face_id,
2074 })
2075 .collect::<Vec<_>>();
2076 if distance < 0.0 {
2077 for fragment in &mut selected {
2078 flip_fragment(fragment)?;
2079 }
2080 }
2081 if distance > 0.0 {
2082 for fragment in &mut chosen_offsets {
2083 flip_fragment(fragment)?;
2084 }
2085 }
2086 face_images.extend(
2087 chosen_offsets
2088 .iter()
2089 .map(|fragment| OffsetShellFaceImageRecord {
2090 role: OffsetFaceRole::Offset,
2091 source_face_id: carriers[fragment.operand as usize].source_face_id,
2092 }),
2093 );
2094 face_images.extend(
2095 chosen_walls
2096 .iter()
2097 .map(|fragment| OffsetShellFaceImageRecord {
2098 role: OffsetFaceRole::Wall,
2099 source_face_id: carriers[fragment.operand as usize].source_face_id,
2100 }),
2101 );
2102 selected.extend(chosen_offsets);
2103 selected.extend(chosen_walls);
2104 let mut solid = assemble_open_fragments(
2105 selected,
2106 &assembly_sources,
2107 &imprint,
2108 2e-3f64.max(scale * 5e-5),
2109 )?;
2110 orient_open_solid_faces(&mut solid)?;
2111 let connector_images = complete_sharp_offset_connectors(
2112 &mut solid,
2113 source,
2114 &face_images,
2115 distance,
2116 2e-3f64.max(scale * 5e-5),
2117 )?;
2118 os_debug!(
2119 "sharp connector completion added {} faces",
2120 connector_images.len()
2121 );
2122 if !connector_images.is_empty() {
2123 // Wall fragments were arranged before the sharp seam was closed, so
2124 // their prospective inner boundary was an open chain. Rebuild the
2125 // wall from the now-closed source/offset cycles instead of retaining
2126 // that pre-connector fragment.
2127 for shell in &mut solid.shells {
2128 shell.faces.retain(|face| {
2129 !matches!(
2130 face_images
2131 .get(face.id.saturating_sub(1) as usize)
2132 .map(|image| image.role),
2133 Some(OffsetFaceRole::Wall)
2134 )
2135 });
2136 }
2137 solid.shells.retain(|shell| !shell.faces.is_empty());
2138 }
2139 face_images.extend(connector_images);
2140 orient_open_solid_faces(&mut solid)?;
2141 // Weld curved-face offset rims that the wall fragmentation left orphaned
2142 // into their coplanar opening cap (cylinder / straight-hole shells). This
2143 // is the seam-rim consistency step the offset pipeline previously lacked.
2144 let welded_rims = weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
2145 os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
2146 // A rim that is not a circle: a free-form or non-iso rim whose offset image
2147 // falls short of the opening. It runs after every earlier weld because it
2148 // only ever sees rims none of them closed.
2149 let rim_band = weld_free_form_rim_bands(
2150 &mut solid,
2151 &mut face_images,
2152 &carriers,
2153 source,
2154 2e-3f64.max(scale * 5e-5),
2155 )?;
2156 let rim_bands = rim_band.welded;
2157 os_debug!("welded {rim_bands} free-form rim bands");
2158 // Weld leftover coaxial coplanar rim PAIRS (a through-hole's exposed wall
2159 // thickness at each opening) into new planar annulus faces.
2160 let pair_rims =
2161 weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5))?;
2162 os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
2163 if debug_enabled() {
2164 for (shell_index, shell) in solid.shells.iter().enumerate() {
2165 for face in &shell.faces {
2166 os_debug!(
2167 "POSTWELD shell {} face {} same_sense={} loops={}",
2168 shell_index,
2169 face.id,
2170 face.same_sense,
2171 face.loops.len()
2172 );
2173 for (loop_index, loop_record) in face.loops.iter().enumerate() {
2174 let walk = loop_record
2175 .coedges
2176 .iter()
2177 .map(|coedge| {
2178 let spin = (|| -> Result<f64, String> {
2179 let [d0, d1] = coedge.pcurve.domain()?;
2180 let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45)?;
2181 let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55)?;
2182 let pa = face.surface.evaluate(a.x, a.y)?;
2183 let pb = face.surface.evaluate(b.x, b.y)?;
2184 Ok(pa.cross(pb).z)
2185 })()
2186 .unwrap_or(f64::NAN);
2187 format!(
2188 "{}{}(spin{:+.0})",
2189 if coedge.forward { "+" } else { "-" },
2190 coedge.edge_id,
2191 spin.signum()
2192 )
2193 })
2194 .collect::<Vec<_>>();
2195 os_debug!(" loop {} {:?}", loop_index, walk);
2196 }
2197 }
2198 }
2199 }
2200 let completion_carriers = carriers.iter().collect::<Vec<_>>();
2201 let completion_tolerance = 2e-3f64.max(scale * 5e-5);
2202 // Unify geometric duplicate arc records BEFORE completion: a ring that is
2203 // already covered by both its faces (frustum + apex cap) must not read as
2204 // an open boundary, or completion papers over it with duplicate faces.
2205 let duplicate_welds = weld_duplicate_one_use_arcs(&mut solid, completion_tolerance)?;
2206 os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
2207 if debug_enabled() {
2208 let mut use_counts = HashMap::<u64, usize>::default();
2209 for coedge in solid
2210 .shells
2211 .iter()
2212 .flat_map(|shell| &shell.faces)
2213 .flat_map(|face| &face.loops)
2214 .flat_map(|loop_record| &loop_record.coedges)
2215 {
2216 *use_counts.entry(coedge.edge_id).or_default() += 1;
2217 }
2218 let open = use_counts.values().filter(|count| **count == 1).count();
2219 os_debug!(
2220 "assembled: faces={} open_edges={open}",
2221 solid
2222 .shells
2223 .iter()
2224 .map(|shell| shell.faces.len())
2225 .sum::<usize>()
2226 );
2227 }
2228 let completed_images = complete_opening_boundary_cycles(
2229 &mut solid,
2230 &completion_carriers,
2231 &face_images,
2232 completion_tolerance,
2233 )?;
2234 os_debug!("completion added {} faces", completed_images.len());
2235 face_images.extend(completed_images);
2236 let solid = merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9))?;
2237 let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
2238 // No faceted fallback here: an offset shell must be made of real analytic
2239 // surfaces. Failing loudly beats silently returning a tessellated BREP
2240 // with destroyed face provenance.
2241 // A shell the rim band lane recognized but could not close fails here as it
2242 // always did; the refusal now carries the lane's reason.
2243 let mut solid = finalize_assembled_solid(solid, assembly_tolerance).map_err(|error| {
2244 let message = String::from(error);
2245 match &rim_band.decline {
2246 Some(reason) => format!("{message}; offset_shell: {reason}"),
2247 None => message,
2248 }
2249 })?;
2250 if rim_bands + pair_rims > 0 {
2251 // A rim-band or rim-pair weld joins independently-oriented skins;
2252 // put the whole finalized manifold on one normal convention so the
2253 // shell reports its exact wall volume (coplanar-only shells keep
2254 // their assembled sense).
2255 coheres_face_normals(&mut solid)?;
2256 }
2257 let solid = solid;
2258 face_images = solid
2259 .shells
2260 .iter()
2261 .flat_map(|shell| &shell.faces)
2262 .map(|face| {
2263 face_images
2264 .get(face.id.saturating_sub(1) as usize)
2265 .cloned()
2266 .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
2267 })
2268 .collect::<Result<Vec<_>, _>>()?;
2269 // A bore's end ring carries the bore wall's provenance whichever
2270 // construction built it (the opening's fragmentation or the rim-pair
2271 // weld), so its name does not depend on the path the rims took.
2272 let bore_rings = claim_bore_end_rings(&solid, &mut face_images)?;
2273 os_debug!("{bore_rings} bore end rings took the bore wall's provenance");
2274 // Honesty gate: a real rim left one-use is a hole in the shell that the
2275 // degenerate marking hides from `validate()` (degenerate edges are allowed
2276 // to be face-local). Refuse such a result instead of shipping a
2277 // non-watertight solid as valid. A rim is a CLOSED edge whose interior
2278 // sweeps measurably away from its seam vertex — a genuine circle, not a
2279 // legitimate pole point placeholder (which stays one-use by design).
2280 let mut rim_use_counts = HashMap::<u64, usize>::default();
2281 for coedge in solid
2282 .shells
2283 .iter()
2284 .flat_map(|shell| &shell.faces)
2285 .flat_map(|face| &face.loops)
2286 .flat_map(|loop_record| &loop_record.coedges)
2287 {
2288 *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
2289 }
2290 if debug_enabled() {
2291 for (shell_index, shell) in solid.shells.iter().enumerate() {
2292 for face in &shell.faces {
2293 let role = face_images
2294 .get(face.id.saturating_sub(1) as usize)
2295 .map(|image| image.role);
2296 let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
2297 os_debug!(
2298 "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
2299 shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
2300 face.loops.len(), c.x, c.y, c.z
2301 );
2302 for (li, lp) in face.loops.iter().enumerate() {
2303 let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
2304 os_debug!(" loop {} edges {:?}", li, ids);
2305 }
2306 }
2307 }
2308 for edge in &solid.edges {
2309 let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
2310 let s = edge.curve.evaluate(edge.t0)?;
2311 let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5)?;
2312 let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25)?;
2313 os_debug!(
2314 "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
2315 edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
2316 edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
2317 s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
2318 );
2319 }
2320 }
2321 for edge in &solid.edges {
2322 if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
2323 || edge.start_vertex_id != edge.end_vertex_id
2324 {
2325 continue;
2326 }
2327 let anchor = edge.curve.evaluate(edge.t0)?;
2328 let sweep_threshold = 1e-4f64.max(scale * 1e-6);
2329 let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
2330 edge.curve
2331 .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
2332 .map(|point| point.sub(anchor).length() > sweep_threshold)
2333 .unwrap_or(false)
2334 });
2335 if sweeps {
2336 if debug_enabled() {
2337 let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5)?;
2338 os_debug!(
2339 "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
2340 edge.id,
2341 anchor.x,
2342 anchor.y,
2343 anchor.z,
2344 rim_mid.x,
2345 rim_mid.y,
2346 rim_mid.z
2347 );
2348 for (shell_index, shell) in solid.shells.iter().enumerate() {
2349 for face in &shell.faces {
2350 let uses_rim = face
2351 .loops
2352 .iter()
2353 .flat_map(|l| &l.coedges)
2354 .any(|c| c.edge_id == edge.id);
2355 let affine = face.surface.is_affine().unwrap_or(false);
2356 let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
2357 .unwrap_or(false);
2358 let proj = project_point_to_surface(&face.surface, rim_mid)?;
2359 let contains = if on {
2360 parameter_point_in_face(
2361 face,
2362 Vec2 {
2363 x: proj.u,
2364 y: proj.v,
2365 },
2366 2e-3f64.max(scale * 5e-5),
2367 )? != PolygonClass::Outside
2368 } else {
2369 false
2370 };
2371 if on || uses_rim {
2372 os_debug!(
2373 " face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
2374 face.id, shell_index, affine, uses_rim, on, proj.distance, contains
2375 );
2376 }
2377 }
2378 }
2379 }
2380 return Err(format!(
2381 "offset_shell: unwelded rim leaves a non-watertight shell \
2382 (one-use closed edge {} near ({:.3},{:.3},{:.3})); this \
2383 curved-solid opening is not yet supported",
2384 edge.id, anchor.x, anchor.y, anchor.z
2385 ));
2386 }
2387 }
2388 // The shell's single exit. An offset skin that crosses another wall, or
2389 // folds through itself where the source turned tighter than the distance,
2390 // leaves a body that validates and measures — so it is asked here, and
2391 // repaired or refused by name rather than returned.
2392 let solid = crate::accept_sound(solid, "offsetShell")?;
2393 Ok(OffsetShellResultRecord { solid, face_images })
2394}
2395
2396/// Run offset shell with stable counters and validation diagnostics suitable
2397/// for automated regressions and UI presentation.
2398pub fn offset_shell_with_diagnostics(
2399 source: &BrepSolid,
2400 opening_face_ids: &[u64],
2401 distance: f64,
2402 tolerances: Option<KernelTolerances>,
2403) -> Result<KernelOutcome<OffsetShellResultRecord>, String> {
2404 let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
2405 policy.check()?;
2406 let mut diagnostics = KernelDiagnostics::default();
2407 diagnostics.count_n(
2408 "collect.source_faces",
2409 source
2410 .shells
2411 .iter()
2412 .map(|shell| shell.faces.len() as u64)
2413 .sum(),
2414 );
2415 diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
2416 diagnostics.measure_max("offset.distance", distance.abs());
2417 diagnostics.measure_max("tolerance.model", policy.model);
2418 // Tally every pcurve fit the shell makes (its own re-trims and any boolean
2419 // it runs) so `pcurve.unmet_floor` reports a trim that stopped at the
2420 // sample ceiling instead of passing it off as one that met the floor.
2421 let fit_scope = crate::PcurveFitScope::open();
2422 // Same for the polyline fits of its carrier×carrier sections, which is
2423 // where a marched rim's accuracy is decided (`geometry/fit.rs`).
2424 let polyline_scope = crate::PolylineFitScope::open();
2425 let result = offset_shell(source, opening_face_ids, distance)?;
2426 fit_scope.close().report_into(&mut diagnostics);
2427 polyline_scope.close().report_into(&mut diagnostics);
2428 diagnostics.count_n(
2429 "sew.output_faces",
2430 result
2431 .solid
2432 .shells
2433 .iter()
2434 .map(|shell| shell.faces.len() as u64)
2435 .sum(),
2436 );
2437 diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
2438 let validation = result.solid.validate_detailed(&policy);
2439 diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
2440 diagnostics.count_n("validate.issues", validation.issues.len() as u64);
2441 diagnostics.count_n(
2442 "validate.wire_warnings",
2443 validation.wire_warnings.len() as u64,
2444 );
2445 for warning in validation.wire_warnings {
2446 diagnostics.event(
2447 DiagnosticSeverity::Warning,
2448 KernelStage::Validate,
2449 "validate.uv_wire",
2450 warning.message,
2451 );
2452 }
2453 if !validation.issues.is_empty() {
2454 for issue in &validation.issues {
2455 diagnostics.event(
2456 DiagnosticSeverity::Error,
2457 KernelStage::Validate,
2458 "validate.brep",
2459 issue.message.clone(),
2460 );
2461 }
2462 return Err(format!(
2463 "offset_shell: invalid diagnostic result: {:?}",
2464 validation.issues
2465 ));
2466 }
2467 Ok(KernelOutcome {
2468 value: result,
2469 diagnostics,
2470 })
2471}