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