Skip to content

fix(algo): merge overlapping deepened wall openings in the internal-loops splitter#1104

Merged
andymai merged 2 commits into
mainfrom
fix/deepened-wall-opening-union
Jul 17, 2026
Merged

fix(algo): merge overlapping deepened wall openings in the internal-loops splitter#1104
andymai merged 2 commits into
mainfrom
fix/deepened-wall-opening-union

Conversation

@andymai

@andymai andymai commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

The snapClip join-edges baseplate export (baseplateGenerator.scenario.snapClip, 5×4 right/front/back join) failed with 5 non-manifold STL edges, and its 44-hole connector chain ran almost entirely on mesh-fallback booleans. Fresh operand capture on 2.126.13 localized the break to op-cut-3: the plate flips from 615 analytic faces to an 8203-face mesh fallback and never recovers.

Root cause. The snap-slot cutter stack cuts the same seam wall twice: hole-2 opens a window in the x=105 join wall, and hole-3 (whose top floats 0.01 above hole-2's floor) re-opens an overlapping window in that same wall. Hole-3's sections form a closed internal loop on the wall face that OVERLAPS the existing inner wire. plane_internal_line_loops only tests loops against the OUTER boundary, so the face routed to split_face_with_internal_loops, which attached the loop as an independent second hole. The two wires double-cover the 0.01 overlap band: both rims stay as unpaired edges, the collinear band pieces trace twice (use=3 micro verticals), the analytic gate rejects the cut, and every subsequent cut inherits the fallback. This is the plane-face twin of the cone stranded-rim case closed in #1102 — the roadmap's deepened-notch terminal row.

Fix. A union pre-pass in split_face_with_internal_loops, gated to plane faces with all-Line loop+hole: detect genuine geometric overlap (proper crossing, vertex containment, or collinear overlap span), split both edge sets at mutual crossings and vertex T-points, classify pieces by midpoint (In/On/Out), and emit ONE merged opening outline (collinear On-pieces contributed once, from the hole's copy so they pair with the pave-split neighbor faces) plus the removable disc bounded by loop-pieces-outside + hole-pieces-inside. All 2D tests project 3D endpoints through one locally built PlaneFrame — stored hole-wire UVs can be fitted in a foreign frame (the pcurve-convention lesson). Any chaining failure bails to the previous behavior.

Result. The full 44-hole join-edges chain now replays analytic and watertight natively: final F=881 (539 plane / 182 cylinder / 160 cone), position-quantized edge pairing clean — versus F=8207 with 86 boundary edges before. Synthetic 3-box fixture included.

Verification

  • New fixture crates/io/tests/deepened_wall_opening_inmem.rs: stranded-rim pairing, single union hole on the wall, exact volume — 10× flake gate clean
  • Full io suite 30/30 targets green (groove-mouth, junction-disc, snap-slot, cornerclip, divider-lip, honeycomb, halfSockets, intwidth, fracwidth foils all green)
  • d4 canary 27/27 (cargo test -p brepkit-wasm --lib gridfinity)
  • brepkit-algo 157 + brepkit-operations 765 green; check-boundaries.sh clean
  • Full workspace suite green via pre-push hook

Roadmap updated in the same PR (terminal row retired; nozzle-chain residual re-scoped with its dig recipe).


Summary by cubic

Merges overlapping deepened wall openings in the internal-loops splitter so plane walls keep a single union hole and cuts stay analytic. Fixes the snapClip join-edges export by preventing double-covered rims and mesh fallback.

  • Bug Fixes
    • Added a union pre-pass in split_face_with_internal_loops for plane faces with all-Line loop+hole: detect overlap (crossing, containment, or collinear span), split at intersections/T-points, classify pieces, and emit one union hole plus the correct removable disc; epsilons derive from kernel tolerance (tol*100), and all 2D tests project 3D endpoints through one local PlaneFrame built from wire_pts; bails to the old path on any failure.
    • Prevents unpaired rims and double-traced bands; the 44-hole snapClip plate now replays analytic and watertight (F=881, posBad=0; was F=8207 with 86 boundary edges).
    • Added crates/io/tests/deepened_wall_opening_inmem.rs to guard the stranded-rim case; updated roadmap notes; all test suites pass.

Written for commit c131cf1. Summary will update on new commits.

Review in cubic

…oops splitter

A later cut can re-open a wall area an earlier cut already opened, offset
slightly (the snapClip slot stack's 0.01 ledge margins). The cutter's
sections form a closed internal loop on the wall face that overlaps the
existing inner wire; the internal-loops splitter attached the loop as an
independent second hole, double-covering the overlap band — both rims
stayed unpaired, the collinear band pieces traced twice, the analytic gate
rejected the cut, and the whole 44-hole snapClip join-edges chain fell
back to mesh booleans (F=615 -> 8203 at op-cut-3, export nm=5).

split_face_with_internal_loops now merges each overlapping pair: the frame
keeps ONE union outline and the removable disc is bounded by the loop
pieces outside the hole plus the hole pieces inside the loop. Gated to
plane faces with all-Line loop+hole; every 2D test projects 3D endpoints
through one locally built frame (stored hole-wire UVs can be fitted in a
foreign frame); any chain failure bails to the previous behavior.

The full snapClip join-edges chain now replays analytic and watertight
natively (F=881 posBad=0, was F=8207 bnd=86).
Copilot AI review requested due to automatic review settings July 17, 2026 15:05
@github-actions

Copy link
Copy Markdown
Contributor

WASM Binary Size

File main PR Delta
brepkit_wasm.wasm 5.85 MB 5.87 MB +19.1 KB (+.3%)

⚠️ Size increased slightly.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a planar-face edge case in brepkit-algo’s internal-loops face splitter where a later cut’s internal section loop can overlap an existing inner wire (deepened/re-opened opening), previously producing double-covered rims, unpaired edges, and triggering mesh-fallback booleans downstream. Adds an in-memory IO fixture to lock the regression and updates the roadmap notes.

Changes:

  • Add a planar “deepened-opening union” pre-pass in split_face_with_internal_loops to merge an overlapping internal loop with an existing hole wire into a single union opening (with a merged removable disc outline).
  • Add deepened_wall_opening_inmem integration test to verify watertight edge pairing, single merged wall hole, and expected volume.
  • Update roadmap documentation to mark the deepened-notch plane-face variant as closed.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
crates/io/tests/deepened_wall_opening_inmem.rs New regression fixture validating the deepened wall-opening union behavior (watertightness, single wall hole, volume).
crates/algo/src/builder/face_splitter/special_cases.rs Implements the planar overlap-detection + union/chain logic inside split_face_with_internal_loops.
.claude/skills/roadmap/SKILL.md Documents closure of the plane-face deepened-opening case and updates remaining work items.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +96 to +99
let e = topo.edge(oe.edge()).unwrap();
(topo.vertex(e.start()).unwrap().point().x() - 10.0).abs() < 1e-9
&& (topo.vertex(e.end()).unwrap().point().x() - 10.0).abs() < 1e-9
});
Comment on lines +1667 to +1670
let lp: Vec<Point2> = ls.iter().map(|s| s.0).collect();
let hp: Vec<Point2> = hs.iter().map(|s| s.0).collect();
let eps = 1e-5;
for &(a0, a1) in &ls {
Comment on lines +1747 to +1751
Out,
}
let eps = 1e-5;
let seg_of = |e: &OrientedPCurveEdge| (frame.project(e.start_3d), frame.project(e.end_3d));
let lsegs: Vec<(Point2, Point2)> = loop_edges.iter().map(seg_of).collect();
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a non-manifold STL export failure in the snapClip join-edges chain by adding a union pre-pass in split_face_with_internal_loops. When a later boolean cut's section loop overlaps an existing inner wire on a plane wall (the 0.01 snap-slot ledge case), the two wires previously double-covered the overlap band — both rims stayed unpaired, collinear segments were traced twice, and the analytic gate rejected every subsequent cut, sending the full 44-hole chain to mesh fallback.

  • Core fix (special_cases.rs): For FaceSurface::Plane faces with all-Line loop+hole, three new functions (internal_loop_hole_interact, union_internal_loop_with_hole, chain_single_closed_loop) detect genuine geometric overlap, split both edge sets at mutual intersection/T-points, classify pieces by midpoint, and emit one merged union outline plus a removable disc. Any chaining failure bails cleanly to the previous independent-wires behavior.
  • New regression fixture (deepened_wall_opening_inmem.rs): Validates watertight output, exactly one inner wire on the wall, and correct volume (900.0 ± 0.05) for the two-cut overlapping-opening scenario.

Confidence Score: 5/5

Safe to merge — the union pre-pass is correctly gated to all-Line plane faces and bails cleanly to the pre-existing independent-wires path on any chaining failure, so no new failure mode is introduced.

All three new helpers carry explicit bail-to-None guards; the 3D-endpoint chain-closure check in chain_single_closed_loop prevents any unclosed loop from being emitted; the regression fixture validates watertight output, correct hole count, and exact volume; io suite and d4 canary are reported fully green.

No files require special attention; both P2 observations are edge cases beyond the target rectangular-overlap shape and are safely covered by the existing bail path.

Important Files Changed

Filename Overview
crates/algo/src/builder/face_splitter/special_cases.rs Core fix: adds the deepened-opening union pre-pass with three well-guarded helper functions; bail paths are safe and consistent with established codebase patterns.
crates/io/tests/deepened_wall_opening_inmem.rs New regression test: validates watertight output, exactly one union hole on the wall face, and correct volume for the two-cut overlapping-opening scenario.
.claude/skills/roadmap/SKILL.md Roadmap update: retires the plane-face deepened-notch terminal row, documents the closed root, and re-scopes the 0.6mm-nozzle residual with its dig recipe.

Reviews (2): Last reviewed commit: "fix(algo): derive union pre-pass epsilon..." | Re-trigger Greptile

Comment thread crates/algo/src/builder/face_splitter/special_cases.rs Outdated
Address review: the weld-band proximity threshold in the deepened-opening
union is now tol*100 instead of a hardcoded 1e-5 (same value, tied to the
kernel tolerance), the wire_pts parameter drops its stale underscore now
that it drives the PlaneFrame construction, and the fixture's wall-face
detection uses a tolerance-derived epsilon.
@andymai
andymai enabled auto-merge (squash) July 17, 2026 15:17
@andymai
andymai merged commit ac46c58 into main Jul 17, 2026
21 checks passed
@andymai
andymai deleted the fix/deepened-wall-opening-union branch July 17, 2026 15:24
@brepkit brepkit Bot mentioned this pull request Jul 17, 2026
brepkit Bot added a commit that referenced this pull request Jul 17, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.126.14](v2.126.13...v2.126.14)
(2026-07-17)


### Bug Fixes

* **algo:** merge overlapping deepened wall openings in the
internal-loops splitter
([#1104](#1104))
([ac46c58](ac46c58))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Release 2.126.14 for `brepkit-wasm`, fixing the internal-loops splitter
to merge overlapping deepened wall openings. This prevents duplicated
openings and keeps wall topology valid.

<sup>Written for commit e887026.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/andymai/brepkit/pull/1105?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

Co-authored-by: brepkit[bot] <265643962+brepkit[bot]@users.noreply.github.com>
andymai added a commit that referenced this pull request Jul 17, 2026
… arcs (#1106)

## Summary

Second half of the snapClip join-edges export root (follows #1104). The
export baseplate builds with the simplified **tapered** pockets, so each
deep relief cutter's back corner lands on a pocket corner **cone**. The
cutter should contribute three sections to that cone face: two marched
conics (cone × back wall, cone × side wall) and the cone × cutter-top
arc that closes the chain.

**Root cause.** A horizontal plane cuts a cone in an exact
`EdgeCurve::Circle`. The exact-arc path that bypasses the sampling
pre-filters (`trim_ellipse_to_boundary_crossings` — built for the
tilted-tread × cylinder family) only accepted **Ellipse** sections, so
the circle fell to the generic 16-sample in-both AABB filter, which
cannot find a ~0.11-long in-both arc on an ~18-long circle. With the
closing arc missing, the cone face received an open two-conic chain, the
internal-loops splitter rejected it (open chains are dropped by design),
and the face stayed unsplit. The resulting analytic-but-leaky solid
(posBad=8) was accepted by the by-edge-id gate and poisoned the export
chain into mesh fallback two cuts later (final bnd=111 nm=6 at export
tolerance).

**Fix.** The trimmer now dispatches over Circle and Ellipse sections
through one angular parameterization (`SecCurve`); emitted arcs keep
their exact curve type so downstream circle-calibrated machinery sees
real Circle sections. No other behavior changes — Ellipse handling is
byte-identical.

**Result.** The minimal repro (`cut(plate-after-op-cut-2,
deep-relief-cutter)`) goes posBad 8→0 on both the raw GFA and ops paths,
and the full 44-hole export-variant join-edges chain replays analytic
and watertight: final **F=418, posBad=0** (was F=4842, bnd=111, nm=6).

## Verification

- New fixture `crates/io/tests/snapclip_export_corner_inmem.rs` (tool's
serialized operands, 2026-07-17 export-variant capture): edge pairing +
analytic-cone assertions, 10× flake gate clean
- Full io suite green (including the tilted-tread/halfSockets ramp
landscapes that calibrate the ellipse path)
- d4 canary 27/27; `brepkit-algo` and `brepkit-operations` suites green
- Full workspace suite green via pre-push hook

Roadmap updated in the same PR. Known remaining snapClip residuals (out
of scope, recorded with repro recipes): the 0.6mm-nozzle export chain's
op-cut-3 (posBad=10, different landscape), the by-edge-id acceptance
gate's blindness to position-duplicate leaks, and the bed-flat clip
volume pin.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes trimming for plane×cone intersections by treating circle sections
as exact boundary-crossing arcs. Restores the short cutter-top closing
arcs, so cone faces split and the snapClip export chain stays analytic
and watertight.

- **Bug Fixes**
- Updated `trim_ellipse_to_boundary_crossings` to handle both
`EdgeCurve::Circle` and `EdgeCurve::Ellipse` via a shared angular
parameterization (`SecCurve`); emitted arcs keep their exact type.
- Avoids dropping ~0.11-length circle arcs in the 16-sample in-both
filter that left open two-conic chains on cone faces.
- Added `crates/io/tests/snapclip_export_corner_inmem.rs` with
serialized operands to assert analytic cones and B-Rep edge pairing.
- Result: minimal repro goes posBad 8→0; full 44-hole export variant
replays analytic `F=418, posBad=0` (was `F=4842, bnd=111, nm=6`).

<sup>Written for commit ca4ce75.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/andymai/brepkit/pull/1106?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants