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