Skip to main content

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
36pub fn offset_shell(
37    source: &BrepSolid,
38    opening_face_ids: &[u64],
39    distance: f64,
40) -> Result<OffsetShellResultRecord, String> {
41    // First try with oblique hole-wall carriers extended past their opening
42    // planes (the lane that closes tilted/conical through-holes). If that
43    // extension fired but the arrangement could not assemble the harder
44    // configuration it produced (e.g. the extended bore genuinely intersects
45    // other offset walls), fall back to the un-extended pipeline so every
46    // previously-supported shape keeps its exact path and every refusal keeps
47    // the canonical honesty-gate message.
48    let mut extension_fired = false;
49    let mut reflex_rebuilds = 0usize;
50    match offset_shell_impl(
51        source,
52        opening_face_ids,
53        distance,
54        true,
55        &mut extension_fired,
56        &mut reflex_rebuilds,
57    ) {
58        Ok(result) => {
59            // Attribution marker: which pipeline produced this result. A
60            // retry-produced Ok can silently mask a broken extension lane —
61            // always say which attempt won. The reflex-rebuild count rides
62            // along because that lane fires on BOTH attempts: "extension
63            // idle" alone would misread as "the plain pipeline did this".
64            os_debug!(
65                "offset_shell result: FIRST attempt (extension {}, reflex-rebuilt {})",
66                if extension_fired { "fired" } else { "idle" },
67                reflex_rebuilds,
68            );
69            Ok(result)
70        }
71        Err(error) if extension_fired => {
72            os_debug!("extended pipeline failed ({error}); retrying without carrier extension");
73            let mut unused = false;
74            let mut retry_reflex = 0usize;
75            let retried = offset_shell_impl(
76                source,
77                opening_face_ids,
78                distance,
79                false,
80                &mut unused,
81                &mut retry_reflex,
82            );
83            os_debug!(
84                "offset_shell result: RETRY without extension ({}, reflex-rebuilt {retry_reflex})",
85                if retried.is_ok() { "ok" } else { "err" },
86            );
87            retried
88        }
89        Err(error) => Err(error),
90    }
91}
92
93fn offset_shell_impl(
94    source: &BrepSolid,
95    opening_face_ids: &[u64],
96    distance: f64,
97    extend_oblique_carriers: bool,
98    extension_fired: &mut bool,
99    reflex_rebuilds: &mut usize,
100) -> Result<OffsetShellResultRecord, String> {
101    if !distance.is_finite() || distance.abs() <= 1e-7 {
102        return Err("offset_shell: distance must be finite and non-zero".into());
103    }
104    if opening_face_ids.is_empty() {
105        return Err("offset_shell: at least one opening face is required".into());
106    }
107    let source_faces = source
108        .shells
109        .iter()
110        .flat_map(|shell| &shell.faces)
111        .collect::<Vec<_>>();
112    let opening_set = opening_face_ids.iter().copied().collect::<HashSet<_>>();
113    if opening_set
114        .iter()
115        .any(|id| !source_faces.iter().any(|face| face.id == *id))
116    {
117        return Err("offset_shell: opening face does not belong to source".into());
118    }
119    let retained = source_faces
120        .iter()
121        .filter(|face| !opening_set.contains(&face.id))
122        .map(|face| face.id)
123        .collect::<Vec<_>>();
124    if retained.is_empty() {
125        return Err("offset_shell: removing every face cannot produce a shell".into());
126    }
127    let mut carriers = Vec::new();
128    for face_id in &retained {
129        // A planar carrier whose source face meets a neighbour at a REFLEX
130        // (concave) edge extends its trim by |distance|: at such a junction
131        // the offset skin must GROW past the source footprint to meet the
132        // neighbour's offset (a pocket wall's source [5,15] → skin
133        // [3.5,16.5]); a source-sized trim leaves a gap no miter can bridge.
134        // The surplus at the face's convex ends is clipped by the imprints +
135        // on-skin filter. Convex-only faces keep their exact source-sized
136        // trim — extending them perturbs delicately-selected fragment
137        // landscapes for no benefit.
138        let miter = source_faces
139            .iter()
140            .find(|face| face.id == *face_id)
141            .map(|face| face_reflex_miter_tan(source, face))
142            .transpose()?
143            .flatten();
144        // The offsets of two faces joined at a reflex edge meet d·tan(θn/2)
145        // past the source rim — d only for a perpendicular join. Scale the
146        // extension to the worst sampled miter (×1.5 headroom so the true
147        // intersection lands strictly INSIDE the extended trim, never on its
148        // boundary where the arrangement drops boundary-coincident cuts) and
149        // cap it: a near-tangential join would ask for an unbounded trim.
150        let reflex_extension = miter.map(|worst_tan| {
151            (distance.abs() * worst_tan * 1.5)
152                .max(distance.abs())
153                .min(distance.abs() * 12.0)
154        });
155        if let Some(extension) = reflex_extension {
156            os_debug!("carrier for src face {face_id}: reflex-extended by {extension:.4}");
157        }
158        carriers.push(Carrier {
159            solid: carrier_solid(
160                source,
161                *face_id,
162                distance,
163                match (distance < 0.0, reflex_extension) {
164                    (true, extension) => distance.abs().max(extension.unwrap_or(0.0)),
165                    (false, Some(extension)) => extension,
166                    (false, None) => 0.0,
167                },
168            )?,
169            source_face_id: *face_id,
170            kind: OffsetFaceRole::Offset,
171        });
172    }
173    // APEX-CAP carriers: a retained face whose DEGENERATE edge (an apex
174    // point, e.g. a conic crater's tip) offsets into a RING is an EXTERIOR
175    // cone offset — the parallel surface truncates at the ring and the exact
176    // rolling-ball offset closes it with a SPHERE of radius |distance| about
177    // the apex. Emit that sphere as an extra carrier: the ring imprint
178    // (sphere × offset frustum) splits it, the on-skin filter keeps exactly
179    // the cap sector (its points are |d| from the apex and ≥|d| from the
180    // rest of the face), and the ring becomes two-use.
181    let mut apex_cap_sources = HashSet::default();
182    let mut apex_cap_pairs: Vec<(usize, usize)> = Vec::new();
183    {
184        let retained_count = carriers.len();
185        for index in 0..retained_count {
186            let source_face = source_by_id_lookup(&source_faces, carriers[index].source_face_id);
187            let Some(source_face) = source_face else {
188                continue;
189            };
190            let carrier_surface = carriers[index].solid.shells[0].faces[0].surface.clone();
191            for coedge in source_face
192                .loops
193                .iter()
194                .flat_map(|loop_record| &loop_record.coedges)
195            {
196                let Some(edge) = source
197                    .edges
198                    .iter()
199                    .find(|edge| edge.id == coedge.edge_id)
200                else {
201                    continue;
202                };
203                if !edge.degenerate {
204                    continue;
205                }
206                // Image of the degenerate edge on the offset surface.
207                let [p0, p1] = coedge.pcurve.domain()?;
208                let mut ring = Vec::new();
209                for sample in 0..=8 {
210                    let uv = coedge
211                        .pcurve
212                        .evaluate(p0 + (p1 - p0) * sample as f64 / 8.0)?;
213                    ring.push(carrier_surface.evaluate(uv.x, uv.y)?);
214                }
215                let spread = ring
216                    .iter()
217                    .map(|point| point.sub(ring[0]).length())
218                    .fold(0.0f64, f64::max);
219                if spread <= distance.abs() * 1e-2 {
220                    continue;
221                }
222                let apex = source
223                    .vertices
224                    .iter()
225                    .find(|vertex| vertex.id == edge.start_vertex_id)
226                    .map(|vertex| vertex.point)
227                    .ok_or_else(|| "offset_shell: apex vertex missing".to_string())?;
228                let centroid = ring
229                    .iter()
230                    .fold(Vec3::default(), |sum, point| sum.add(*point))
231                    .scale(1.0 / ring.len() as f64);
232                let axis = apex.sub(centroid);
233                let axis = if axis.length() > 1e-9 {
234                    axis.normalized()?
235                } else {
236                    Vec3::new(0.0, 0.0, 1.0)
237                };
238                os_debug!(
239                    "apex-cap carrier for src face {} at ({:.3},{:.3},{:.3}) r={:.3}",
240                    carriers[index].source_face_id,
241                    apex.x,
242                    apex.y,
243                    apex.z,
244                    distance.abs(),
245                );
246                apex_cap_sources.insert(index);
247                apex_cap_sources.insert(carriers.len());
248                apex_cap_pairs.push((index, carriers.len()));
249                carriers.push(Carrier {
250                    solid: crate::make_sphere_brep(apex, distance.abs(), axis)?,
251                    source_face_id: carriers[index].source_face_id,
252                    kind: OffsetFaceRole::Offset,
253                });
254            }
255        }
256    }
257    for face_id in opening_face_ids {
258        let source_face = source_faces
259            .iter()
260            .find(|face| face.id == *face_id)
261            .unwrap();
262        carriers.push(Carrier {
263            solid: if distance < 0.0 && source_face.surface.is_affine()? {
264                let mut wall = carrier_solid(source, *face_id, 0.0, distance.abs())?;
265                // The pad extends the wall by STRETCHING the plane's 2x2 net
266                // while the trim pcurves are cloned in UV, so every loop
267                // scales about the patch centre by (w+2|d|)/w. For the OUTER
268                // loop of a full-domain rectangle that lands exactly on the
269                // grown outline — the intended wall extent. But an INTERIOR
270                // loop (a carve outline: a bore, gouge or crater rim in the
271                // opening face) is scaled OUTWARD too, and the enlarged hole
272                // then swallows both circles the wall must be cut by (the
273                // source carve rim from the wall x source imprint and the
274                // shrunk rim from the wall x offset-carrier imprint) — the
275                // imprints clip to nothing and the rims dangle one-use. Drop
276                // interior loops instead: the wall covers the carve mouth,
277                // the imprints cut the true rims, and selection (void-wall
278                // guard + on-skin filter) drops the fragments over the void.
279                drop_wall_interior_loops(&mut wall)?;
280                wall
281            } else {
282                standalone_face(source, *face_id)?
283            },
284            source_face_id: *face_id,
285            kind: OffsetFaceRole::Wall,
286        });
287    }
288    if carriers.len() >= SOURCE_OPERAND as usize {
289        return Err("offset_shell: too many carrier faces for current ABI".into());
290    }
291    // The characteristic length every tolerance below is derived from.  This
292    // was `max ‖vertex‖` — the distance from the WORLD ORIGIN — until audit
293    // slice 0: that form made every band widen as the part was moved away from
294    // the origin while ignoring the part's actual size, so the same shell
295    // solved to different tolerances depending only on where it was modelled.
296    // `solid_scale` is the bbox diagonal, floored at 1.0 exactly as the old
297    // fold was, so this changes ONE thing: placement-dependence.
298    let scale = crate::solid_scale(source);
299    crate::report_scale_migration("offset_shell", scale, || {
300        source
301            .vertices
302            .iter()
303            .map(|vertex| vertex.point.length())
304            .fold(1.0, f64::max)
305    });
306    let smooth = synchronize_smooth_offset_boundaries(&mut carriers, &source_faces, scale)?;
307    // Reflex-rim rebuilds run on BOTH attempts: the fallback for a broken
308    // extension is an honest refusal, but the fallback for a skipped reflex
309    // rebuild is a silently WRONG (membrane-capped) shell.
310    let reflex_rebuilt = rebuild_reflex_rim_carriers(
311        &mut carriers,
312        source,
313        &source_faces,
314        &smooth.pairs,
315        scale,
316        distance,
317    )?;
318    *reflex_rebuilds = reflex_rebuilt.len();
319    if extend_oblique_carriers {
320        let oblique_extended = extend_offset_carriers_past_open_hole_rims(
321            &mut carriers,
322            source,
323            &source_faces,
324            &opening_set,
325            &smooth.pairs,
326            scale,
327        )?;
328        let mut already_extended = oblique_extended.clone();
329        already_extended.extend(reflex_rebuilt.iter().copied());
330        let fallshort_extended = extend_fallshort_curved_carriers(
331            &mut carriers,
332            source,
333            &source_faces,
334            &opening_set,
335            &smooth.pairs,
336            &already_extended,
337            scale,
338            distance,
339        )?;
340        *extension_fired = !oblique_extended.is_empty() || fallshort_extended > 0;
341        os_debug!(
342            "extended {} oblique + {fallshort_extended} fall-short carriers past their opening planes",
343            oblique_extended.len(),
344        );
345    }
346    let tolerance = 1e-7f64.max(scale * 1e-9);
347    let pair_tolerance = (scale * 1e-8).max(2e-6);
348    let reach = distance.abs() * 4.0 + pair_tolerance;
349    let carrier_samples = carriers
350        .iter()
351        .map(|carrier| carrier_extent_points(&carrier.solid))
352        .collect::<Result<Vec<_>, _>>()?;
353    let carrier_bounds = carrier_samples
354        .iter()
355        .map(|samples| Bounds::from_points(samples))
356        .collect::<Option<Vec<_>>>()
357        .ok_or_else(|| "offset_shell: carrier has no boundary samples".to_string())?;
358    let source_by_id = source_faces
359        .iter()
360        .map(|face| (face.id, *face))
361        .collect::<HashMap<_, _>>();
362    let source_samples = source_faces
363        .iter()
364        .map(|face| {
365            let standalone = standalone_face(source, face.id)?;
366            Ok((face.id, edge_sample_points(&standalone)?))
367        })
368        .collect::<Result<HashMap<_, _>, String>>()?;
369    if debug_enabled() {
370        for (index, carrier) in carriers.iter().enumerate() {
371            let face = &carrier.solid.shells[0].faces[0];
372            let samples = &carrier_samples[index];
373            let bounds =
374                samples
375                    .iter()
376                    .fold((Vec3::default(), Vec3::default()), |(low, high), point| {
377                        (
378                            Vec3 {
379                                x: low.x.min(point.x),
380                                y: low.y.min(point.y),
381                                z: low.z.min(point.z),
382                            },
383                            Vec3 {
384                                x: high.x.max(point.x),
385                                y: high.y.max(point.y),
386                                z: high.z.max(point.z),
387                            },
388                        )
389                    });
390            let surface = &face.surface;
391            let rational = surface
392                .control_points
393                .iter()
394                .flatten()
395                .any(|point| (point.w - 1.0).abs() > 1e-12);
396            os_debug!(
397                "carrier[{index}] kind={:?} source_face={} loops={} deg=({},{}) net={}x{} rational={rational} bounds=({:.3},{:.3},{:.3})..({:.3},{:.3},{:.3})",
398                carrier.kind, carrier.source_face_id, face.loops.len(),
399                surface.degree_u, surface.degree_v,
400                surface.control_points.len(),
401                surface.control_points.first().map(|row| row.len()).unwrap_or(0),
402                bounds.0.x, bounds.0.y, bounds.0.z, bounds.1.x, bounds.1.y, bounds.1.z,
403            );
404        }
405        for (index, carrier) in carriers.iter().enumerate() {
406            if !matches!(carrier.kind, OffsetFaceRole::Offset) {
407                continue;
408            }
409            let source_face = source_by_id[&carrier.source_face_id];
410            let carrier_surface = &carrier.solid.shells[0].faces[0].surface;
411            let (ku, kv) = (
412                crate::KnotVector::new(
413                    source_face.surface.knots_u.clone(),
414                    source_face.surface.degree_u,
415                ),
416                crate::KnotVector::new(
417                    source_face.surface.knots_v.clone(),
418                    source_face.surface.degree_v,
419                ),
420            );
421            if let (Ok(ku), Ok(kv)) = (ku, kv) {
422                let [u0, u1] = ku.domain();
423                let [v0, v1] = kv.domain();
424                let mut worst = 0.0f64;
425                let mut worst_at = (0.0f64, 0.0f64);
426                for iu in 0..=24 {
427                    for iv in 0..=24 {
428                        let u = u0 + (u1 - u0) * iu as f64 / 24.0;
429                        let v = v0 + (v1 - v0) * iv as f64 / 24.0;
430                        let expected = face_offsets(source_face)
431                            .at(u, v, -distance)
432                            .map(|sample| sample.point);
433                        let actual = carrier_surface.evaluate(u, v);
434                        if let (Ok(expected), Ok(actual)) = (expected, actual) {
435                            let error = expected.sub(actual).length();
436                            if error > worst {
437                                worst = error;
438                                worst_at = (u, v);
439                            }
440                        }
441                    }
442                }
443                let mut normal_swing = 0.0f64;
444                let mut previous: Option<Vec3> = None;
445                for iu in 0..=48 {
446                    let u = u0 + (u1 - u0) * iu as f64 / 48.0;
447                    let v = (v0 + v1) / 2.0;
448                    if let Ok(normal) = face_normal(source_face, u, v) {
449                        if let Some(previous) = previous {
450                            normal_swing = normal_swing.max(previous.sub(normal).length());
451                        }
452                        previous = Some(normal);
453                    }
454                }
455                let weights = source_face
456                    .surface
457                    .control_points
458                    .iter()
459                    .flatten()
460                    .map(|point| point.w)
461                    .fold((f64::MAX, f64::MIN), |(low, high), w| {
462                        (low.min(w), high.max(w))
463                    });
464                os_debug!(
465                    "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={:?}",
466                    worst_at.0, worst_at.1,
467                    source_face.surface.control_points.len(),
468                    source_face.surface.control_points.first().map(|row| row.len()).unwrap_or(0),
469                    source_face.surface.degree_u, source_face.surface.degree_v,
470                    weights.0, weights.1,
471                    &source_face.surface.knots_u,
472                );
473            }
474        }
475        os_debug!("smooth pairs: {:?}", smooth.pairs);
476    }
477    let mut imprint = empty_imprint();
478    let mut next_piece_id = 1;
479    let mut next_vertex_id = 1;
480    for first in 0..carriers.len() {
481        for second in first + 1..carriers.len() {
482            if matches!(carriers[first].kind, OffsetFaceRole::Wall)
483                && matches!(carriers[second].kind, OffsetFaceRole::Wall)
484            {
485                continue;
486            }
487            if smooth.pairs.contains(&(first, second)) {
488                os_debug!("pair ({first},{second}) skipped: smooth");
489                continue;
490            }
491            if !carrier_bounds[first].intersects(carrier_bounds[second], pair_tolerance) {
492                os_debug!("pair ({first},{second}) skipped: bounds");
493                continue;
494            }
495            let first_source = source_by_id[&carriers[first].source_face_id];
496            let second_source = source_by_id[&carriers[second].source_face_id];
497            if !source_faces_adjacent(first_source, second_source)
498                && sample_separation(
499                    &source_samples[&first_source.id],
500                    &source_samples[&second_source.id],
501                ) > reach
502            {
503                os_debug!("pair ({first},{second}) skipped: separation");
504                continue;
505            }
506            let pair = match build_imprints(
507                &carriers[first].solid,
508                &carriers[second].solid,
509                &ImprintOptions {
510                    tolerance,
511                    maximum_fit_points: 96,
512                    local_fit: true,
513                    fit_chunk_points: None,
514                    // Offset carriers can meet in short, tightly curved
515                    // branches. The general SSI default (diagonal / 15) is
516                    // too coarse here and lets independently fitted branches
517                    // miss a shared junction when thickness crosses one of
518                    // those branches.
519                    maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
520                },
521            ) {
522                Ok(pair) => pair,
523                // A failed pair imprint (e.g. the marching SSI exhausting its
524                // step budget on a long closed sphere/plane intersection) is
525                // not fatal to the shell: the affected rim simply stays
526                // unsplit and the rim-weld passes below get to close it. The
527                // final watertightness gate still refuses anything the welds
528                // cannot reach, so tolerating the miss only enlarges the
529                // honest success set.
530                Err(error) => {
531                    os_debug!("pair ({first},{second}) imprint failed: {error}");
532                    continue;
533                }
534            };
535            let pieces_before = imprint.pieces.len();
536            merge_pair_imprint(
537                &mut imprint,
538                pair,
539                first as u8,
540                second as u8,
541                &mut next_piece_id,
542                &mut next_vertex_id,
543                tolerance,
544                scale,
545            );
546            os_debug!(
547                "pair ({first},{second}) src=({},{}) pieces+={}",
548                carriers[first].source_face_id,
549                carriers[second].source_face_id,
550                imprint.pieces.len() - pieces_before,
551            );
552        }
553    }
554    if distance < 0.0 {
555        // OUTWARD shells: the opening-wall carrier is the opening face's
556        // plane EXTENDED by |distance|, so its trim carries only the GROWN
557        // outline. Its inner boundary — where the wall ring stops at the
558        // source solid — is the source outline, and the retained SOURCE faces
559        // are not carriers, so no carrier×carrier pair ever imprints it.
560        // Un-cut, the wall stays one whole-plane fragment covering the
561        // opening void, and the source faces' rim edges dangle one-use.
562        // Imprint each wall carrier against the retained source faces that
563        // share an edge with its opening: the wall then fragments into the
564        // ring (kept) and the opening hole (dropped by the void guard).
565        for index in 0..carriers.len() {
566            if !matches!(carriers[index].kind, OffsetFaceRole::Wall) {
567                continue;
568            }
569            let opening = source_by_id[&carriers[index].source_face_id];
570            for retained_id in &retained {
571                let retained_face = source_by_id[retained_id];
572                if !source_faces_adjacent(opening, retained_face) {
573                    continue;
574                }
575                let neighbor = standalone_face(source, *retained_id)?;
576                let pair = match build_imprints(
577                    &carriers[index].solid,
578                    &neighbor,
579                    &ImprintOptions {
580                        tolerance,
581                        maximum_fit_points: 96,
582                        local_fit: true,
583                        fit_chunk_points: None,
584                        maximum_ssi_step: Some((scale * 0.0005).max(tolerance * 100.0)),
585                    },
586                ) {
587                    Ok(pair) => pair,
588                    Err(error) => {
589                        os_debug!(
590                            "wall×source pair ({index},{retained_id}) imprint failed: {error}"
591                        );
592                        continue;
593                    }
594                };
595                let pieces_before = imprint.pieces.len();
596                merge_pair_imprint(
597                    &mut imprint,
598                    pair,
599                    index as u8,
600                    SOURCE_OPERAND,
601                    &mut next_piece_id,
602                    &mut next_vertex_id,
603                    tolerance,
604                    scale,
605                );
606                os_debug!(
607                    "wall×source pair ({index},{retained_id}) pieces+={}",
608                    imprint.pieces.len() - pieces_before,
609                );
610            }
611        }
612    }
613    // APEX-CAP ring splits: the cap × cone section lies ON the cone
614    // carrier's promoted ring edge (an overlap, not a crossing), so
615    // `build_imprints` mints no edge splits for the cone side. Without them
616    // the cone keeps ONE closed ring edge while the cap carries the section
617    // as arcs — mismatched segmentation, both sides one-use. Record the arc
618    // junctions as splits on the cone's coincident edge.
619    for (cone_index, cap_index) in &apex_cap_pairs {
620        let cap_face_id = carriers[*cap_index].solid.shells[0].faces[0].id;
621        let piece_ids = imprint
622            .by_face
623            .iter()
624            .find(|entry| entry.operand == *cap_index as u8 && entry.face_id == cap_face_id)
625            .map(|entry| entry.piece_ids.clone())
626            .unwrap_or_default();
627        let coincidence_band = 2e-5f64.max(scale * 1e-7);
628        for piece_id in piece_ids {
629            let Some((curve, t0, t1)) = imprint
630                .pieces
631                .iter()
632                .find(|piece| piece.id == piece_id)
633                .map(|piece| (piece.curve.clone(), piece.t0, piece.t1))
634            else {
635                continue;
636            };
637            for endpoint in [curve.evaluate(t0)?, curve.evaluate(t1)?] {
638                for edge in &carriers[*cone_index].solid.edges {
639                    if edge.degenerate {
640                        continue;
641                    }
642                    let projection = crate::project_point_to_curve(&edge.curve, endpoint)?;
643                    if projection.distance > coincidence_band
644                        || projection.u < edge.t0 + 1e-9
645                        || projection.u > edge.t1 - 1e-9
646                    {
647                        continue;
648                    }
649                    let parameter = projection.u;
650                    if let Some(existing) = imprint.edge_splits.iter_mut().find(|split| {
651                        split.operand == *cone_index as u8 && split.edge_id == edge.id
652                    }) {
653                        if !existing
654                            .parameters
655                            .iter()
656                            .any(|value| (*value - parameter).abs() <= 1e-8)
657                        {
658                            existing.parameters.push(parameter);
659                        }
660                    } else {
661                        imprint.edge_splits.push(EdgeSplitRecord {
662                            operand: *cone_index as u8,
663                            edge_id: edge.id,
664                            parameters: vec![parameter],
665                        });
666                    }
667                    os_debug!(
668                        "apex-cap ring split: cone carrier {cone_index} edge {} at t={parameter:.6}",
669                        edge.id,
670                    );
671                }
672            }
673        }
674    }
675    if debug_enabled() {
676        for piece in &imprint.pieces {
677            let start = piece.curve.evaluate(piece.t0);
678            let mid = piece.curve.evaluate((piece.t0 + piece.t1) * 0.5);
679            let end = piece.curve.evaluate(piece.t1);
680            if let (Ok(start), Ok(mid), Ok(end)) = (start, mid, end) {
681                os_debug!(
682                    "piece[{}] ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
683                    piece.id,
684                    start.x,
685                    start.y,
686                    start.z,
687                    mid.x,
688                    mid.y,
689                    mid.z,
690                    end.x,
691                    end.y,
692                    end.z,
693                );
694            }
695        }
696    }
697    synchronize_smooth_edge_splits(&mut imprint, &smooth.edge_pairs);
698    // This decides whether an imprint is genuinely the carrier's existing
699    // trim, not whether independently fitted curves can later be sewn. The
700    // looser assembly tolerance can erase a short separating branch while it
701    // is still departing a boundary.
702    let boundary_coincidence_tolerance = 2e-5f64.max(scale * 1e-7);
703    let piece_geometry = imprint
704        .pieces
705        .iter()
706        .map(|piece| {
707            (
708                piece.id,
709                FragmentEdgeGeometry {
710                    curve: piece.curve.clone(),
711                    t0: piece.t0,
712                    t1: piece.t1,
713                },
714            )
715        })
716        .collect::<HashMap<_, _>>();
717    for (index, carrier) in carriers.iter().enumerate() {
718        if !matches!(carrier.kind, OffsetFaceRole::Offset) {
719            continue;
720        }
721        let boundaries = carrier
722            .solid
723            .edges
724            .iter()
725            .map(|edge| FragmentEdgeGeometry {
726                curve: edge.curve.clone(),
727                t0: edge.t0,
728                t1: edge.t1,
729            })
730            .collect::<Vec<_>>();
731        if let Some(by_face) = imprint
732            .by_face
733            .iter_mut()
734            .find(|entry| entry.operand == index as u8)
735        {
736            let before = by_face.piece_ids.clone();
737            by_face.piece_ids.retain(|piece_id| {
738                let piece = &piece_geometry[piece_id];
739                !boundaries.iter().any(|boundary| {
740                    fragment_edge_lies_on(piece, boundary, boundary_coincidence_tolerance)
741                        .unwrap_or(false)
742                })
743            });
744            if debug_enabled() && before.len() != by_face.piece_ids.len() {
745                os_debug!(
746                    "carrier[{index}] dropped boundary-coincident pieces: {:?} -> {:?}",
747                    before,
748                    by_face.piece_ids,
749                );
750            }
751        }
752    }
753
754    if debug_enabled() {
755        for entry in &imprint.by_face {
756            os_debug!(
757                "by_face operand={} face={} pieces={:?}",
758                entry.operand,
759                entry.face_id,
760                entry.piece_ids,
761            );
762        }
763    }
764    let mut split_carriers = Vec::new();
765    let mut fragments_by_carrier = Vec::new();
766    for (index, carrier) in carriers.iter().enumerate() {
767        // Offset carriers keep their boundary un-split (their trims never
768        // coincide with imprints — the boundary-coincidence filter drops such
769        // pieces instead). An apex-cap ring breaks that assumption: the
770        // frustum's ring is its TRIM boundary while the cap crosses it as TWO
771        // imprint arcs — without splitting, the two sides segment differently
772        // and both stay one-use. Split exactly the cap-affected carriers.
773        let split = if matches!(carrier.kind, OffsetFaceRole::Offset)
774            && !apex_cap_sources.contains(&index)
775        {
776            carrier.solid.clone()
777        } else {
778            apply_edge_splits(&carrier.solid, index as u8, &imprint)?
779        };
780        let fragments = fragment_solid(&split, index as u8, &imprint)?;
781        os_debug!(
782            "carrier[{index}] kind={:?} src={} fragments={}",
783            carrier.kind,
784            carrier.source_face_id,
785            fragments.len(),
786        );
787        if debug_enabled() {
788            for (fragment_index, fragment) in fragments.iter().enumerate() {
789                let mut boundary_count = 0usize;
790                let mut imprint_pieces = Vec::new();
791                let mut derived_count = 0usize;
792                for coedge in fragment
793                    .loops
794                    .iter()
795                    .flat_map(|loop_record| &loop_record.coedges)
796                {
797                    match &coedge.source {
798                        FragmentEdgeSource::Boundary { .. }
799                        | FragmentEdgeSource::SharedBoundary { .. } => boundary_count += 1,
800                        FragmentEdgeSource::Imprint { piece_id } => imprint_pieces.push(*piece_id),
801                        FragmentEdgeSource::Derived { .. } => derived_count += 1,
802                    }
803                }
804                os_debug!(
805                    "  frag[{index}.{fragment_index}] test=({:.3},{:.3},{:.3}) uv=({:.3},{:.3}) boundary={boundary_count} derived={derived_count} pieces={imprint_pieces:?}",
806                    fragment.test_point.x, fragment.test_point.y, fragment.test_point.z,
807                    fragment.test_uv.x, fragment.test_uv.y,
808                );
809            }
810        }
811        split_carriers.push(split);
812        fragments_by_carrier.push(fragments);
813    }
814    let mut assembly_sources = [(SOURCE_OPERAND, source)]
815        .into_iter()
816        .collect::<HashMap<_, _>>();
817    for (index, carrier) in split_carriers.iter().enumerate() {
818        assembly_sources.insert(index as u8, carrier);
819    }
820
821    let source_edge_by_id = source
822        .edges
823        .iter()
824        .map(|edge| (edge.id, edge))
825        .collect::<HashMap<_, _>>();
826    let retained_faces_with_edges = retained
827        .iter()
828        .map(|face_id| {
829            let face = source_by_id[face_id];
830            let edges = face
831                .loops
832                .iter()
833                .flat_map(|loop_record| &loop_record.coedges)
834                .filter_map(|coedge| source_edge_by_id.get(&coedge.edge_id).copied())
835                .collect::<Vec<_>>();
836            (face, edges)
837        })
838        .collect::<Vec<_>>();
839    let offset_skin_tolerance = 2e-3f64.max(scale * 5e-5).max(distance.abs() * 1e-3);
840
841    let mut chosen_offsets = Vec::new();
842    let mut chosen_walls = Vec::new();
843    for (index, carrier) in carriers.iter().enumerate() {
844        let seed = source_seed(source, carrier.source_face_id)?;
845        if matches!(carrier.kind, OffsetFaceRole::Offset) {
846            // Fragments whose test point sits at the full offset distance
847            // from every retained source face are the true offset-skin
848            // regions. Fragments closer to some other face are shadowed
849            // pockets/strips that another carrier owns. Only fall back to the
850            // unfiltered set when nothing qualifies (numerical safety net).
851            let on_skin = fragments_by_carrier[index]
852                .iter()
853                .map(|fragment| {
854                    point_on_offset_skin(
855                        fragment.test_point,
856                        &retained_faces_with_edges,
857                        distance,
858                        offset_skin_tolerance,
859                    )
860                })
861                .collect::<Result<Vec<_>, String>>()?;
862            let skin_filter_active = on_skin.iter().any(|flag| *flag);
863            let mut classified = Vec::new();
864            let mut viable = Vec::new();
865            for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
866                let class = classify_point(fragment.test_point, source, tolerance * 10.0)?.class;
867                let excluded_by_class = (distance > 0.0 && class == PointClass::Out)
868                    || (distance < 0.0 && class == PointClass::In);
869                let contact = if excluded_by_class {
870                    false
871                } else {
872                    distance > 0.0
873                        && fragment_has_sustained_source_contact(
874                            fragment,
875                            &source_faces,
876                            &assembly_sources,
877                            &imprint,
878                            2e-3f64.max(scale * 5e-5),
879                        )?
880                };
881                os_debug!(
882                    "  select[{index}.{fragment_index}] class={class:?} contact={contact} on_skin={} seed_in={:?} uv=({:.3},{:.3})",
883                    on_skin[fragment_index],
884                    parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8),
885                    fragment.test_uv.x, fragment.test_uv.y,
886                );
887                if excluded_by_class || (skin_filter_active && !on_skin[fragment_index]) {
888                    continue;
889                }
890                classified.push(fragment.clone());
891                if contact {
892                    continue;
893                }
894                viable.push(fragment.clone());
895            }
896            os_debug!("  select[{index}] seed=({:.4},{:.4})", seed.x, seed.y);
897            let seeded = viable
898                .iter()
899                .filter(|fragment| {
900                    parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
901                        .is_ok_and(|class| class != PolygonClass::Outside)
902                })
903                .cloned()
904                .collect::<Vec<_>>();
905            let seeded_classified = classified
906                .iter()
907                .filter(|fragment| {
908                    parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)
909                        .is_ok_and(|class| class != PolygonClass::Outside)
910                })
911                .cloned()
912                .collect::<Vec<_>>();
913            let selected = if viable.is_empty() {
914                if !skin_filter_active {
915                    // NOTHING on this carrier reads on-skin: every classified
916                    // fragment's test point is measurably CLOSER than the
917                    // offset distance to some retained face — a buried strip
918                    // another carrier owns. Forcing the largest one in anyway
919                    // plants a wrong skin patch that collides with the
920                    // completion phase's reconstruction (3-use edges).
921                    // Contribute nothing: `complete_opening_boundary_cycles`
922                    // rebuilds the true skin from the carrier surface, and the
923                    // watertight gate still refuses if it cannot.
924                    None
925                } else {
926                    let mut by_area = classified
927                        .into_iter()
928                        .map(|fragment| {
929                            let area = parameter_space_area(&fragment_as_trim(&fragment))?.abs();
930                            Ok((fragment, area))
931                        })
932                        .collect::<Result<Vec<_>, String>>()?;
933                    by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
934                    by_area.into_iter().next().map(|entry| entry.0)
935                }
936            } else if !seeded_classified.is_empty() {
937                let mut candidates = seeded_classified;
938                candidates.sort_by(|a, b| {
939                    a.test_uv
940                        .sub(seed)
941                        .length()
942                        .total_cmp(&b.test_uv.sub(seed).length())
943                });
944                candidates.into_iter().next()
945            } else if viable.len() > 2 {
946                let mut by_area = viable
947                    .into_iter()
948                    .map(|fragment| {
949                        let area = parameter_space_area(&fragment_as_trim(&fragment))?.abs();
950                        Ok((fragment, area))
951                    })
952                    .collect::<Result<Vec<_>, String>>()?;
953                by_area.sort_by(|a, b| b.1.total_cmp(&a.1));
954                by_area.into_iter().next().map(|entry| entry.0)
955            } else {
956                let mut candidates = if seeded.is_empty() { viable } else { seeded };
957                candidates.sort_by(|a, b| {
958                    a.test_uv
959                        .sub(seed)
960                        .length()
961                        .total_cmp(&b.test_uv.sub(seed).length())
962                });
963                candidates.into_iter().next()
964            };
965            if let Some(fragment) = selected {
966                chosen_offsets.push(fragment);
967            }
968        } else {
969            // Keep arrangement order stable. Besides making the result
970            // reproducible, this matches the reference kernel's Set insertion
971            // order when the final manifold-wall guard admits fragments.
972            let opening = source_by_id[&carrier.source_face_id];
973            let wall_seeds = opening_wall_seeds(
974                opening,
975                &split_carriers[index].shells[0].faces[0],
976                source,
977                &opening_set,
978                distance,
979            )?;
980            // The wall-seed exemptions below rest on "the coplanar-rim weld
981            // cuts the void hole later" — which only ever happens on PLANAR
982            // opening carriers. A curved carrier (e.g. the sphere face used
983            // directly as the opening) keeps its un-split fragment forever:
984            // exempting it would seal the opening with the source face itself
985            // and fabricate a closed hollow solid. Curved carriers therefore
986            // get the strict void guard.
987            let carrier_planar =
988                surface_is_planar(&split_carriers[index].shells[0].faces[0].surface, tolerance)
989                    .unwrap_or(false);
990            let mut selected_indices = Vec::new();
991            for (fragment_index, fragment) in fragments_by_carrier[index].iter().enumerate() {
992                if parameter_point_in_face(&fragment_as_trim(fragment), seed, 1e-8)?
993                    == PolygonClass::Outside
994                {
995                    // Wall = the material cross-section exposed at the
996                    // opening: every point of a genuine wall fragment lies
997                    // WITHIN the offset distance of some retained face (it is
998                    // the thickness between that face and its offset skin). A
999                    // fragment at ≥ distance from EVERY retained face — and
1000                    // containing no expected wall seed (an un-split cap
1001                    // fragment can span both the void AND the wall annulus;
1002                    // the coplanar-rim weld cuts its hole later) — is the
1003                    // core's own cross-section: a HOLE in the opening (e.g.
1004                    // the corner void where a carve's offset circle passes
1005                    // just inside the opening's corner). Welding it shut
1006                    // would fabricate a wall over the void.
1007                    if point_on_offset_skin(
1008                        fragment.test_point,
1009                        &retained_faces_with_edges,
1010                        distance,
1011                        offset_skin_tolerance,
1012                    )? && !(carrier_planar
1013                        && wall_seeds.iter().any(|wall_seed| {
1014                            parameter_point_in_face(&fragment_as_trim(fragment), *wall_seed, 1e-8)
1015                                .is_ok_and(|class| class != PolygonClass::Outside)
1016                        }))
1017                    {
1018                        os_debug!(
1019                            "  wall[{index}.{fragment_index}] skipped: void cross-section at \
1020                             ({:.3},{:.3},{:.3})",
1021                            fragment.test_point.x,
1022                            fragment.test_point.y,
1023                            fragment.test_point.z,
1024                        );
1025                        continue;
1026                    }
1027                    selected_indices.push(fragment_index);
1028                }
1029            }
1030            for wall_seed in wall_seeds {
1031                let fragments = &fragments_by_carrier[index];
1032                let nearest_containing = fragments
1033                    .iter()
1034                    .enumerate()
1035                    .filter(|(_, fragment)| {
1036                        parameter_point_in_face(&fragment_as_trim(fragment), wall_seed, 1e-8)
1037                            .is_ok_and(|class| class != PolygonClass::Outside)
1038                    })
1039                    .min_by(|(_, first), (_, second)| {
1040                        first
1041                            .test_uv
1042                            .sub(wall_seed)
1043                            .length()
1044                            .total_cmp(&second.test_uv.sub(wall_seed).length())
1045                    })
1046                    .map(|(fragment_index, _)| fragment_index);
1047                // Arrangement fitting can leave the seed just outside every
1048                // fragment by a small amount. Match the reference kernel's
1049                // geometric fallback instead of silently omitting the wall
1050                // fragment and leaving the assembled shell open.
1051                let nearest = if nearest_containing.is_some() {
1052                    nearest_containing
1053                } else {
1054                    let seed_point = split_carriers[index].shells[0].faces[0]
1055                        .surface
1056                        .evaluate(wall_seed.x, wall_seed.y)?;
1057                    fragments
1058                        .iter()
1059                        .enumerate()
1060                        .min_by(|(_, first), (_, second)| {
1061                            first
1062                                .test_point
1063                                .sub(seed_point)
1064                                .length()
1065                                .total_cmp(&second.test_point.sub(seed_point).length())
1066                        })
1067                        .map(|(fragment_index, _)| fragment_index)
1068                };
1069                if let Some(fragment_index) = nearest {
1070                    let candidate = &fragments_by_carrier[index][fragment_index];
1071                    // Void-cross-section guard. On a PLANAR carrier it applies
1072                    // to the GEOMETRIC FALLBACK only: a fragment that
1073                    // genuinely contains this wall seed is wall content by
1074                    // construction (even when un-split it also spans void —
1075                    // the rim weld cuts the hole later), but a merely-nearest
1076                    // fragment must not resurrect a hole in the opening. On a
1077                    // CURVED carrier no later weld cuts the hole, so even a
1078                    // seed-containing fragment gets the strict guard.
1079                    if (nearest_containing.is_none() || !carrier_planar)
1080                        && point_on_offset_skin(
1081                            candidate.test_point,
1082                            &retained_faces_with_edges,
1083                            distance,
1084                            offset_skin_tolerance,
1085                        )?
1086                    {
1087                        os_debug!(
1088                            "  wall[{index}.{fragment_index}] seed-hit skipped: void \
1089                             cross-section (planar={carrier_planar}) at ({:.3},{:.3},{:.3})",
1090                            candidate.test_point.x,
1091                            candidate.test_point.y,
1092                            candidate.test_point.z,
1093                        );
1094                        continue;
1095                    }
1096                    let existing = chosen_offsets.iter().cloned().chain(
1097                        selected_indices
1098                            .iter()
1099                            .map(|selected| fragments_by_carrier[index][*selected].clone()),
1100                    );
1101                    if !selected_indices.contains(&fragment_index)
1102                        && !would_overuse_existing_boundary(
1103                            candidate,
1104                            existing,
1105                            &assembly_sources,
1106                            &imprint,
1107                            2e-3,
1108                        )?
1109                    {
1110                        selected_indices.push(fragment_index);
1111                    }
1112                }
1113            }
1114            for fragment_index in selected_indices.iter().copied() {
1115                chosen_walls.push(fragments_by_carrier[index][fragment_index].clone());
1116            }
1117        }
1118    }
1119    if debug_enabled() {
1120        for fragment in &chosen_offsets {
1121            os_debug!(
1122                "chosen offset: operand={} src={} test=({:.3},{:.3},{:.3})",
1123                fragment.operand,
1124                carriers[fragment.operand as usize].source_face_id,
1125                fragment.test_point.x,
1126                fragment.test_point.y,
1127                fragment.test_point.z,
1128            );
1129            for (loop_index, loop_record) in fragment.loops.iter().enumerate() {
1130                for coedge in &loop_record.coedges {
1131                    let surface = &fragment.surface;
1132                    let [c0, c1] = coedge.pcurve.domain().unwrap_or([0.0, 0.0]);
1133                    let uv0 = coedge.pcurve.evaluate(c0);
1134                    let uv1 = coedge.pcurve.evaluate(c1);
1135                    if let (Ok(uv0), Ok(uv1)) = (uv0, uv1) {
1136                        let p0 = surface.evaluate(uv0.x, uv0.y);
1137                        let p1 = surface.evaluate(uv1.x, uv1.y);
1138                        if let (Ok(p0), Ok(p1)) = (p0, p1) {
1139                            let kind = match &coedge.source {
1140                                FragmentEdgeSource::Boundary { edge_id, .. } => {
1141                                    format!("bnd:{edge_id}")
1142                                }
1143                                FragmentEdgeSource::SharedBoundary { edge_id, .. } => {
1144                                    format!("sbnd:{edge_id}")
1145                                }
1146                                FragmentEdgeSource::Imprint { piece_id } => {
1147                                    format!("imp:{piece_id}")
1148                                }
1149                                FragmentEdgeSource::Derived { .. } => "derived".to_string(),
1150                            };
1151                            os_debug!(
1152                                "    loop{loop_index} {kind} ({:.4},{:.4},{:.4})..({:.4},{:.4},{:.4})",
1153                                p0.x, p0.y, p0.z, p1.x, p1.y, p1.z,
1154                            );
1155                        }
1156                    }
1157                }
1158            }
1159        }
1160        for fragment in &chosen_walls {
1161            os_debug!(
1162                "chosen wall: operand={} src={} test=({:.3},{:.3},{:.3})",
1163                fragment.operand,
1164                carriers[fragment.operand as usize].source_face_id,
1165                fragment.test_point.x,
1166                fragment.test_point.y,
1167                fragment.test_point.z,
1168            );
1169        }
1170    }
1171    if chosen_offsets.is_empty() {
1172        return Err("offset_shell: carrier intersections produced no offset boundary".into());
1173    }
1174    // Opening-wall fragments can overlap at dense carrier junctions. The
1175    // seed-time guard above handles geometric containment; this final pass
1176    // mirrors the topology-level rule and counts exact fragment edge sources.
1177    let mut boundary_use_count = HashMap::<[i64; 9], usize>::default();
1178    for fragment in &chosen_offsets {
1179        for coedge in fragment
1180            .loops
1181            .iter()
1182            .flat_map(|loop_record| &loop_record.coedges)
1183        {
1184            let key = fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint)?;
1185            *boundary_use_count.entry(key).or_default() += 1;
1186        }
1187    }
1188    let mut manifold_walls = Vec::new();
1189    for fragment in chosen_walls {
1190        let keys = fragment
1191            .loops
1192            .iter()
1193            .flat_map(|loop_record| &loop_record.coedges)
1194            .map(|coedge| fragment_edge_segment_key(&coedge.source, &assembly_sources, &imprint))
1195            .collect::<Result<Vec<_>, _>>()?;
1196        if !keys
1197            .iter()
1198            .any(|key| boundary_use_count.get(key).copied().unwrap_or(0) >= 2)
1199        {
1200            for key in keys {
1201                *boundary_use_count.entry(key).or_default() += 1;
1202            }
1203            manifold_walls.push(fragment);
1204        }
1205    }
1206    os_debug!(
1207        "manifold walls kept: {} operands={:?}",
1208        manifold_walls.len(),
1209        manifold_walls
1210            .iter()
1211            .map(|fragment| fragment.operand)
1212            .collect::<Vec<_>>(),
1213    );
1214    let chosen_walls = manifold_walls;
1215
1216    let source_fragments = fragment_solid(source, SOURCE_OPERAND, &empty_imprint())?;
1217    let mut selected = source_fragments
1218        .into_iter()
1219        .filter(|fragment| retained.contains(&fragment.source_face_id))
1220        .collect::<Vec<_>>();
1221    let mut face_images = selected
1222        .iter()
1223        .map(|fragment| OffsetShellFaceImageRecord {
1224            role: OffsetFaceRole::Source,
1225            source_face_id: fragment.source_face_id,
1226        })
1227        .collect::<Vec<_>>();
1228    if distance < 0.0 {
1229        for fragment in &mut selected {
1230            flip_fragment(fragment)?;
1231        }
1232    }
1233    if distance > 0.0 {
1234        for fragment in &mut chosen_offsets {
1235            flip_fragment(fragment)?;
1236        }
1237    }
1238    face_images.extend(
1239        chosen_offsets
1240            .iter()
1241            .map(|fragment| OffsetShellFaceImageRecord {
1242                role: OffsetFaceRole::Offset,
1243                source_face_id: carriers[fragment.operand as usize].source_face_id,
1244            }),
1245    );
1246    face_images.extend(
1247        chosen_walls
1248            .iter()
1249            .map(|fragment| OffsetShellFaceImageRecord {
1250                role: OffsetFaceRole::Wall,
1251                source_face_id: carriers[fragment.operand as usize].source_face_id,
1252            }),
1253    );
1254    selected.extend(chosen_offsets);
1255    selected.extend(chosen_walls);
1256    let mut solid = assemble_open_fragments(
1257        selected,
1258        &assembly_sources,
1259        &imprint,
1260        2e-3f64.max(scale * 5e-5),
1261    )?;
1262    orient_open_solid_faces(&mut solid)?;
1263    let connector_images = complete_sharp_offset_connectors(
1264        &mut solid,
1265        source,
1266        &face_images,
1267        distance,
1268        2e-3f64.max(scale * 5e-5),
1269    )?;
1270    os_debug!(
1271        "sharp connector completion added {} faces",
1272        connector_images.len()
1273    );
1274    if !connector_images.is_empty() {
1275        // Wall fragments were arranged before the sharp seam was closed, so
1276        // their prospective inner boundary was an open chain. Rebuild the
1277        // wall from the now-closed source/offset cycles instead of retaining
1278        // that pre-connector fragment.
1279        for shell in &mut solid.shells {
1280            shell.faces.retain(|face| {
1281                !matches!(
1282                    face_images
1283                        .get(face.id.saturating_sub(1) as usize)
1284                        .map(|image| image.role),
1285                    Some(OffsetFaceRole::Wall)
1286                )
1287            });
1288        }
1289        solid.shells.retain(|shell| !shell.faces.is_empty());
1290    }
1291    face_images.extend(connector_images);
1292    orient_open_solid_faces(&mut solid)?;
1293    // Weld curved-face offset rims that the wall fragmentation left orphaned
1294    // into their coplanar opening cap (cylinder / straight-hole shells). This
1295    // is the seam-rim consistency step the offset pipeline previously lacked.
1296    let welded_rims = weld_coplanar_orphan_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
1297    os_debug!("welded {welded_rims} coplanar orphan rims into opening caps");
1298    // Weld oblique-wall offset rims (truncated cone) into a ruled frustum
1299    // opening-wall band, rebuilding the flat cap the pipeline mis-builds.
1300    let ruled_rims = weld_ruled_offset_rims(&mut solid, 2e-3f64.max(scale * 5e-5))?;
1301    os_debug!("welded {ruled_rims} non-coplanar offset rims into ruled bands");
1302    // Weld leftover coaxial coplanar rim PAIRS (a through-hole's exposed wall
1303    // thickness at each opening) into new planar annulus faces.
1304    let pair_rims =
1305        weld_coplanar_rim_pair_annuli(&mut solid, &mut face_images, 2e-3f64.max(scale * 5e-5))?;
1306    os_debug!("welded {pair_rims} coplanar rim pairs into annulus walls");
1307    if debug_enabled() {
1308        for (shell_index, shell) in solid.shells.iter().enumerate() {
1309            for face in &shell.faces {
1310                os_debug!(
1311                    "POSTWELD shell {} face {} same_sense={} loops={}",
1312                    shell_index,
1313                    face.id,
1314                    face.same_sense,
1315                    face.loops.len()
1316                );
1317                for (loop_index, loop_record) in face.loops.iter().enumerate() {
1318                    let walk = loop_record
1319                        .coedges
1320                        .iter()
1321                        .map(|coedge| {
1322                            let spin = (|| -> Result<f64, String> {
1323                                let [d0, d1] = coedge.pcurve.domain()?;
1324                                let a = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.45)?;
1325                                let b = coedge.pcurve.evaluate(d0 + (d1 - d0) * 0.55)?;
1326                                let pa = face.surface.evaluate(a.x, a.y)?;
1327                                let pb = face.surface.evaluate(b.x, b.y)?;
1328                                Ok(pa.cross(pb).z)
1329                            })()
1330                            .unwrap_or(f64::NAN);
1331                            format!(
1332                                "{}{}(spin{:+.0})",
1333                                if coedge.forward { "+" } else { "-" },
1334                                coedge.edge_id,
1335                                spin.signum()
1336                            )
1337                        })
1338                        .collect::<Vec<_>>();
1339                    os_debug!("    loop {} {:?}", loop_index, walk);
1340                }
1341            }
1342        }
1343    }
1344    let completion_carriers = carriers.iter().collect::<Vec<_>>();
1345    let completion_tolerance = 2e-3f64.max(scale * 5e-5);
1346    // Unify geometric duplicate arc records BEFORE completion: a ring that is
1347    // already covered by both its faces (frustum + apex cap) must not read as
1348    // an open boundary, or completion papers over it with duplicate faces.
1349    let duplicate_welds = weld_duplicate_one_use_arcs(&mut solid, completion_tolerance)?;
1350    os_debug!("welded {duplicate_welds} duplicate one-use boundary arcs");
1351    if debug_enabled() {
1352        let mut use_counts = HashMap::<u64, usize>::default();
1353        for coedge in solid
1354            .shells
1355            .iter()
1356            .flat_map(|shell| &shell.faces)
1357            .flat_map(|face| &face.loops)
1358            .flat_map(|loop_record| &loop_record.coedges)
1359        {
1360            *use_counts.entry(coedge.edge_id).or_default() += 1;
1361        }
1362        let open = use_counts.values().filter(|count| **count == 1).count();
1363        os_debug!(
1364            "assembled: faces={} open_edges={open}",
1365            solid
1366                .shells
1367                .iter()
1368                .map(|shell| shell.faces.len())
1369                .sum::<usize>()
1370        );
1371    }
1372    let completed_images = complete_opening_boundary_cycles(
1373        &mut solid,
1374        &completion_carriers,
1375        &face_images,
1376        completion_tolerance,
1377    )?;
1378    os_debug!("completion added {} faces", completed_images.len());
1379    face_images.extend(completed_images);
1380    let solid = merge_same_surface_faces_open(&solid, (1e-7f64).max(scale * 1e-9))?;
1381    let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
1382    // No faceted fallback here: an offset shell must be made of real analytic
1383    // surfaces. Failing loudly beats silently returning a tessellated BREP
1384    // with destroyed face provenance.
1385    let mut solid = finalize_assembled_solid(solid, assembly_tolerance)?;
1386    if ruled_rims + pair_rims > 0 {
1387        // A ruled-band or rim-pair weld joins independently-oriented skins;
1388        // put the whole finalized manifold on one normal convention so the
1389        // shell reports its exact wall volume (coplanar-only shells keep
1390        // their assembled sense).
1391        coheres_face_normals(&mut solid)?;
1392    }
1393    let solid = solid;
1394    face_images = solid
1395        .shells
1396        .iter()
1397        .flat_map(|shell| &shell.faces)
1398        .map(|face| {
1399            face_images
1400                .get(face.id.saturating_sub(1) as usize)
1401                .cloned()
1402                .ok_or_else(|| "offset_shell: merged face lost provenance".to_string())
1403        })
1404        .collect::<Result<Vec<_>, _>>()?;
1405    // Honesty gate: a real rim left one-use is a hole in the shell that the
1406    // degenerate marking hides from `validate()` (degenerate edges are allowed
1407    // to be face-local). Refuse such a result instead of shipping a
1408    // non-watertight solid as valid. A rim is a CLOSED edge whose interior
1409    // sweeps measurably away from its seam vertex — a genuine circle, not a
1410    // legitimate pole point placeholder (which stays one-use by design).
1411    let mut rim_use_counts = HashMap::<u64, usize>::default();
1412    for coedge in solid
1413        .shells
1414        .iter()
1415        .flat_map(|shell| &shell.faces)
1416        .flat_map(|face| &face.loops)
1417        .flat_map(|loop_record| &loop_record.coedges)
1418    {
1419        *rim_use_counts.entry(coedge.edge_id).or_default() += 1;
1420    }
1421    if debug_enabled() {
1422        for (shell_index, shell) in solid.shells.iter().enumerate() {
1423            for face in &shell.faces {
1424                let role = face_images
1425                    .get(face.id.saturating_sub(1) as usize)
1426                    .map(|image| image.role);
1427                let c = face.surface.evaluate(0.5, 0.5).unwrap_or_default();
1428                os_debug!(
1429                    "DUMPFACE shell {} face {} affine={} role={:?} loops={} center=({:.3},{:.3},{:.3})",
1430                    shell_index, face.id, face.surface.is_affine().unwrap_or(false), role,
1431                    face.loops.len(), c.x, c.y, c.z
1432                );
1433                for (li, lp) in face.loops.iter().enumerate() {
1434                    let ids = lp.coedges.iter().map(|c| c.edge_id).collect::<Vec<_>>();
1435                    os_debug!("    loop {} edges {:?}", li, ids);
1436                }
1437            }
1438        }
1439        for edge in &solid.edges {
1440            let uses = rim_use_counts.get(&edge.id).copied().unwrap_or(0);
1441            let s = edge.curve.evaluate(edge.t0)?;
1442            let m = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5)?;
1443            let q = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * 0.25)?;
1444            os_debug!(
1445                "DUMPEDGE edge {} uses={} closed={} deg={} cpts={} cdeg={} knots={:?} start=({:.3},{:.3},{:.3}) q=({:.3},{:.3},{:.3}) mid=({:.3},{:.3},{:.3})",
1446                edge.id, uses, edge.start_vertex_id == edge.end_vertex_id, edge.degenerate,
1447                edge.curve.control_points.len(), edge.curve.degree, edge.curve.knots,
1448                s.x, s.y, s.z, q.x, q.y, q.z, m.x, m.y, m.z
1449            );
1450        }
1451    }
1452    for edge in &solid.edges {
1453        if rim_use_counts.get(&edge.id).copied().unwrap_or(0) != 1
1454            || edge.start_vertex_id != edge.end_vertex_id
1455        {
1456            continue;
1457        }
1458        let anchor = edge.curve.evaluate(edge.t0)?;
1459        let sweep_threshold = 1e-4f64.max(scale * 1e-6);
1460        let sweeps = [0.25, 0.5, 0.75].into_iter().any(|fraction| {
1461            edge.curve
1462                .evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
1463                .map(|point| point.sub(anchor).length() > sweep_threshold)
1464                .unwrap_or(false)
1465        });
1466        if sweeps {
1467            if debug_enabled() {
1468                let rim_mid = edge.curve.evaluate((edge.t0 + edge.t1) * 0.5)?;
1469                os_debug!(
1470                    "PROBE orphan rim edge {} anchor=({:.4},{:.4},{:.4}) mid=({:.4},{:.4},{:.4})",
1471                    edge.id,
1472                    anchor.x,
1473                    anchor.y,
1474                    anchor.z,
1475                    rim_mid.x,
1476                    rim_mid.y,
1477                    rim_mid.z
1478                );
1479                for (shell_index, shell) in solid.shells.iter().enumerate() {
1480                    for face in &shell.faces {
1481                        let uses_rim = face
1482                            .loops
1483                            .iter()
1484                            .flat_map(|l| &l.coedges)
1485                            .any(|c| c.edge_id == edge.id);
1486                        let affine = face.surface.is_affine().unwrap_or(false);
1487                        let on = edge_on_surface(edge, &face.surface, 2e-3f64.max(scale * 5e-5))
1488                            .unwrap_or(false);
1489                        let proj = project_point_to_surface(&face.surface, rim_mid)?;
1490                        let contains = if on {
1491                            parameter_point_in_face(
1492                                face,
1493                                Vec2 {
1494                                    x: proj.u,
1495                                    y: proj.v,
1496                                },
1497                                2e-3f64.max(scale * 5e-5),
1498                            )? != PolygonClass::Outside
1499                        } else {
1500                            false
1501                        };
1502                        if on || uses_rim {
1503                            os_debug!(
1504                                "  face {} shell {} affine={} uses_rim={} on_surf={} proj_dist={:.5} contains={}",
1505                                face.id, shell_index, affine, uses_rim, on, proj.distance, contains
1506                            );
1507                        }
1508                    }
1509                }
1510            }
1511            return Err(format!(
1512                "offset_shell: unwelded rim leaves a non-watertight shell \
1513                 (one-use closed edge {} near ({:.3},{:.3},{:.3})); this \
1514                 curved-solid opening is not yet supported",
1515                edge.id, anchor.x, anchor.y, anchor.z
1516            ));
1517        }
1518    }
1519    Ok(OffsetShellResultRecord { solid, face_images })
1520}
1521
1522/// Run offset shell with stable counters and validation diagnostics suitable
1523/// for automated regressions and UI presentation.
1524pub fn offset_shell_with_diagnostics(
1525    source: &BrepSolid,
1526    opening_face_ids: &[u64],
1527    distance: f64,
1528    tolerances: Option<KernelTolerances>,
1529) -> Result<KernelOutcome<OffsetShellResultRecord>, String> {
1530    let policy = tolerances.unwrap_or_else(|| KernelTolerances::for_solid(source, 1e-7));
1531    policy.check()?;
1532    let mut diagnostics = KernelDiagnostics::default();
1533    diagnostics.count_n(
1534        "collect.source_faces",
1535        source
1536            .shells
1537            .iter()
1538            .map(|shell| shell.faces.len() as u64)
1539            .sum(),
1540    );
1541    diagnostics.count_n("collect.opening_faces", opening_face_ids.len() as u64);
1542    diagnostics.measure_max("offset.distance", distance.abs());
1543    diagnostics.measure_max("tolerance.model", policy.model);
1544    let result = offset_shell(source, opening_face_ids, distance)?;
1545    diagnostics.count_n(
1546        "sew.output_faces",
1547        result
1548            .solid
1549            .shells
1550            .iter()
1551            .map(|shell| shell.faces.len() as u64)
1552            .sum(),
1553    );
1554    diagnostics.count_n("sew.face_images", result.face_images.len() as u64);
1555    let validation = result.solid.validate_detailed(&policy);
1556    diagnostics.measure_max("validate.max_pcurve_error", validation.max_pcurve_error);
1557    diagnostics.count_n("validate.issues", validation.issues.len() as u64);
1558    diagnostics.count_n(
1559        "validate.wire_warnings",
1560        validation.wire_warnings.len() as u64,
1561    );
1562    for warning in validation.wire_warnings {
1563        diagnostics.event(
1564            DiagnosticSeverity::Warning,
1565            KernelStage::Validate,
1566            "validate.uv_wire",
1567            warning.message,
1568        );
1569    }
1570    if !validation.issues.is_empty() {
1571        for issue in &validation.issues {
1572            diagnostics.event(
1573                DiagnosticSeverity::Error,
1574                KernelStage::Validate,
1575                "validate.brep",
1576                issue.message.clone(),
1577            );
1578        }
1579        return Err(format!(
1580            "offset_shell: invalid diagnostic result: {:?}",
1581            validation.issues
1582        ));
1583    }
1584    Ok(KernelOutcome {
1585        value: result,
1586        diagnostics,
1587    })
1588}