Skip to content

fix(algo): adopt existing boundary vertices as seams for closed section curves#755

Merged
andymai merged 6 commits into
mainfrom
fix/gfa-closed-section-seam-adoption
Jun 9, 2026
Merged

fix(algo): adopt existing boundary vertices as seams for closed section curves#755
andymai merged 6 commits into
mainfrom
fix/gfa-closed-section-seam-adoption

Conversation

@andymai

@andymai andymai commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Closed section circles (e.g. a cylinder cap coplanar with a box face) were created with a fresh seam vertex at an arbitrary angle. link_existing matches section-to-boundary pave blocks by quantized endpoint-pair key, so the new circle never linked with the coincident existing boundary circle: duplicate coincident edges survived, same-domain detection found zero pairs, and the result failed validation (orphan cap disc, 3-face edges, Euler 5) and fell through to the mesh fallback.

Changes

  • phase_ff: when a closed section curve carries an existing boundary vertex on it (within tolerance), adopt that vertex as the seam and re-anchor t_range there — gated to circles that do not properly cross either face's boundary (the un-gated version regressed fuse_two_cylinders).
  • link_existing: secondary geometric index for full closed circle blocks keyed by quantized center/radius/axis-up-to-sign.
  • fill_images_faces: skip closed sections coincident with a face's own closed boundary edge; builder interior-point sampling handles unsplit periodic faces bounded by closed curves.
  • Fixes three brepkit-check face-integration bugs the new absolute-volume assertion exposed: wire chaining by connectivity instead of orientation flags, planar inner-wire hole subtraction, seam-aligned closed-edge sampling.
  • New regression test gfa_cut_box_cylinder_coplanar_caps_produces_valid_topology (closed manifold, exact volume); each algo change verified load-bearing by selective revert.
  • compound_cut_matches_sequential_2x2_grid: oracle tightened to absolute volume (the old two-path relative check passed on garbage); stays ignored — cuts 2–4 still drop faces carrying pre-existing hole loops (the band-splitter PR addresses that path).

Verification

Full workspace suite green; algo 95, check 44, operations 629 lib; fmt, clippy -D warnings, boundary check (check added as algo dev-dependency only).

andymai added 3 commits June 9, 2026 12:44
Wires assembled by the boolean engine store edges in loop order but with
arbitrary per-edge orientation flags; face_polygon trusted the flags and
produced folded polygons (a 4x2 rectangle integrated as area 4). Polygons
are now chained by vertex connectivity.

Planar faces now subtract inner-wire (hole) contributions instead of
over-integrating the hole region.

Closed boundary circles are sampled starting at their seam vertex in the
traversal direction, so periodic-face UV trimming polygons close into the
seam corners instead of cutting diagonals across the domain.
…on curves

A closed section circle (e.g. the wall-plane intersection of a cylinder
whose caps are coplanar with the other solid's faces) previously got a
fresh seam vertex at an arbitrary angle. The coincident existing boundary
circle keeps its own seam, so link_existing's endpoint-pair key never
matched, duplicate coincident circle edges survived, edge-set SD pairing
found nothing, and the result was non-manifold (3-face circle edges,
orphan cap disc) with fall-through to the mesh fallback.

- phase_ff: before allocating a seam vertex for a closed section circle,
  scan both faces' boundary vertices for one lying on the curve, adopt it
  and re-anchor t_range there. Gated to circles that do not properly cross
  either face's boundary (line-segment and coplanar circle-circle checks)
  so partially-overlapping rims keep the legacy fresh-seam path.
- link_existing: secondary index for full closed circle blocks keyed by
  quantized center/radius/axis (up to sign) so geometric matching links
  coincident full circles even when seam adoption misses.
- fill_images_faces: skip closed section curves that coincide with one of
  the face's own closed boundary edges — they lie entirely on the
  boundary and would corrupt the wire if fed to the splitter.
- builder: interior-point sampling for unsplit periodic faces bounded by
  closed curves uses the closed edge's u at the v-range midpoint.
- regression test: box(4,4,2) minus r=0.3 cylinder with caps coplanar at
  z=0/z=2 asserts closed manifold, Euler 2, no coincident full-circle
  edges, and volume within 0.1% of the analytic value.
The relative compound-vs-sequential oracle was vacuous: both paths agreed
at ~27% below the true volume because cuts after the first fall back to
the mesh path. The oracle now checks both paths against the analytic
volume; the test stays ignored with the actual remaining gap documented
(face splitter drops faces carrying pre-existing internal hole loops when
a new coplanar closed section arrives).
Copilot AI review requested due to automatic review settings June 9, 2026 20:21
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

WASM Binary Size

File main PR Delta
brepkit_wasm.wasm 4.70 MB 4.71 MB +10.6 KB (+.2%)

⚠️ 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

This PR fixes a class of boolean-operation failures where closed section circles (notably coplanar cylinder caps) were created with arbitrary seam vertices and therefore failed to link against coincident existing boundary circles, leading to duplicate coincident edges, missed same-domain pairing, and invalid/non-manifold topology (often falling back to mesh boolean).

Changes:

  • Adopt existing boundary vertices as seam vertices for eligible closed section circles (and re-anchor the parametric range), plus add a geometry-based fallback index to link coincident full-circle blocks even when seam vertices differ.
  • Avoid corrupting face splitting by skipping closed sections that coincide with a face’s own closed boundary edge; improve interior-point sampling for periodic faces bounded by closed curves.
  • Strengthen/extend topology and volume validation: new coplanar-cap regression test, improved planar face integration (subtract inner wires), and tighter absolute-volume oracles in ignored compound-cut tests.

Reviewed changes

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

Show a summary per file
File Description
crates/operations/src/boolean/tests.rs Tightens an ignored compound-cut test oracle to absolute expected volume and documents the remaining known gap.
crates/check/src/util.rs Adds wire_polygon and improves polygon construction by chaining edges via vertex connectivity; improves closed-edge sampling anchored at seam vertices.
crates/check/src/properties/face_integrator.rs Improves periodic UV bounds handling and subtracts inner-wire (hole) contributions for planar faces.
crates/algo/src/pave_filler/tests.rs Adds regression test for coplanar-cap cylinder cut validity (manifold + Euler + volume).
crates/algo/src/pave_filler/phase_ff.rs Implements seam adoption for eligible closed section circles using existing boundary vertices; adds boundary-crossing gating.
crates/algo/src/pave_filler/link_existing.rs Adds a secondary closed-circle geometric index and refactors linking into try_link.
crates/algo/src/builder/mod.rs Adjusts face interior sampling for periodic/non-planar faces bounded by closed curves.
crates/algo/src/builder/fill_images_faces.rs Skips closed section curves that coincide with a face’s own closed boundary edge to prevent wire corruption.
crates/algo/Cargo.toml Adds brepkit-check as an algo dev-dependency for property-based verification in tests.

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

Comment on lines +226 to +228
if periodic_u && u_max - u_min < 1e-9 {
u_max = u_min + std::f64::consts::TAU;
}
Comment on lines +229 to +231
if periodic_v && v_max - v_min < 1e-9 {
v_max = v_min + std::f64::consts::TAU;
}
Comment on lines +1258 to +1264
let ellipses_match = |a: &brepkit_math::curves::Ellipse3D,
b: &brepkit_math::curves::Ellipse3D| {
(a.center() - b.center()).length() < tol
&& (a.semi_major() - b.semi_major()).abs() < tol
&& (a.semi_minor() - b.semi_minor()).abs() < tol
&& a.normal().dot(b.normal()).abs() > 1.0 - 1e-9
};
Comment thread crates/algo/src/pave_filler/tests.rs Outdated
}
}

let expected_vol = 32.0 - std::f64::consts::PI * 0.09 * 2.0;
@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown

Greptile Summary

Three coordinated algo fixes eliminate the duplicate-coincident-circle defect for coplanar-cap boolean operations: phase_ff adopts an existing boundary vertex as the section circle's seam when the circle stays inside both faces; link_existing adds a secondary closed_index keyed by quantized circle geometry to match seam-mismatched but geometrically identical circles; and fill_images_faces skips feeding a coincident section circle to the face splitter altogether. Five brepkit-check integration bugs are fixed alongside: connectivity-based wire chaining, planar inner-wire hole subtraction, seam-aligned closed-edge polygon sampling, seam-collapsed periodic UV range expansion, and a closed-edge interior-point sampling path for periodic faces.

  • phase_ff/link_existing/fill_images_faces: three-way fix for the coplanar-cap seam mismatch causing orphan cap discs, 3-face edges, and Euler-5 failures, gated behind a crossing check to avoid regressing fuse_two_cylinders.
  • face_integrator/util: inner wire hole subtraction is now correctly applied to the planar integrator; wire_polygon derives traversal direction from vertex connectivity rather than orientation flags to fix wire chaining; the UV seam-collapse heuristic handles full-revolution lateral faces.
  • tests: new regression test validates Euler characteristic, manifold edge-use counts, no duplicate coincident circles, and exact volume to 0.1%; compound_cut_matches_sequential_2x2_grid oracle is tightened to absolute expected volume with a more descriptive ignore message.

Confidence Score: 5/5

Safe to merge; the three-way seam fix is load-tested by selective revert, full suite green, and the new volume test anchors correctness to 0.1%.

All algo and check logic paths are carefully gated: the seam-adoption path fires only when closed_circle_crosses_face_boundaries returns false, preventing regression on partial-arc cases; the closed_index fallback is independently harmless if no match is found; inner-wire hole subtraction uses unsigned tri_area = cross.length() * 0.5 so sign is winding-direction-independent. No new unwrap, expect, or float == comparisons are introduced outside tests. The one remaining gap — the hardcoded 1e-9 seam-collapse threshold in face_uv_bounds — is a speculative concern for geometrically degenerate sliver faces and does not affect any tested path.

No files require special attention; the phase_ff and face_integrator changes each have focused scope, and the new test exercises the critical coplanar-cap path end-to-end.

Important Files Changed

Filename Overview
crates/algo/src/pave_filler/phase_ff.rs Adds seam-adoption path for closed section circles: new find_boundary_vertex_on_curve and closed_circle_crosses_face_boundaries helpers gate the adoption correctly; the ? propagation issue in the vertex loop and the Ellipse/NurbsCurve crossing gap were flagged in previous reviews.
crates/algo/src/pave_filler/link_existing.rs Refactors the linking loop into try_link and adds a closed_index secondary index keyed by quantized circle geometry; axis canonicalization in circle_key is correct (v/-v pair produces identical key). Circle-only scope already noted in previous reviews.
crates/algo/src/builder/fill_images_faces.rs Adds closed_curve_coincides_with_boundary to skip feeding coincident closed section curves to the face splitter; correctly checks both outer and inner wires, handles Circle and Ellipse with tolerance-based matching.
crates/algo/src/builder/mod.rs Adds periodic-face interior sampling path in sample_face_interior; iterates edges (a &[OrientedEdge] slice) twice which is safe, computes v-midpoint for closed-edge bounded faces. The v_max = f64::MIN initializer is correct and consistent with the existing code style.
crates/check/src/properties/face_integrator.rs Adds inner-wire hole subtraction to integrate_planar_face and fixes the seam-collapsed UV range in face_uv_bounds; tri_area = cross.length() * 0.5 is always positive so hole subtraction is sign-correct regardless of winding. The 1e-9 threshold in face_uv_bounds is hardcoded rather than derived from the model's Tolerance.
crates/check/src/util.rs Adds public wire_polygon using connectivity-based direction derivation; correctly seam-aligns closed-curve sampling to c.project(seam_pt) and handles forward/reversed cases. NURBS closed curves fall back to unseamed sample_edge_curve. Oriented-edge flag deviation noted in previous reviews.
crates/algo/src/pave_filler/tests.rs New regression test gfa_cut_box_cylinder_coplanar_caps_produces_valid_topology checks Euler characteristic, manifold edge-use counts, no duplicate coincident circles, and exact volume to 0.1% with Gauss order 16.
crates/operations/src/boolean/tests.rs Tightens compound_cut_matches_sequential_2x2_grid oracle to absolute expected volume; updates the #[ignore] message to document the remaining gap accurately.
crates/algo/Cargo.toml Adds brepkit-check as a dev-dependency only; check is L1.5 (depends only on math/topology/geometry), so algo (L2) using it in tests does not create a circular dependency or layer violation.

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
crates/check/src/properties/face_integrator.rs:226-231
**Hardcoded threshold may incorrectly expand a genuinely zero-span periodic UV region**

`u_max - u_min < 1e-9` fires whenever all sampled UV points are within 1 nm of each other in parameter space on a periodic axis — correct for the intended "all vertices on the seam" case. However, a legitimately degenerate face (e.g. a cylinder sliver where the topology is valid but the geometry has zero angular extent) would also trigger this branch and silently expand its integration domain to the full `[0, 2π]` period, producing an incorrect area/volume contribution. `face_uv_bounds` has no `Tolerance` parameter to derive a model-relative threshold; a follow-up to thread `tol` through here would let the threshold scale with the model.

Reviews (2): Last reviewed commit: "test(algo): derive coplanar-cap cut expe..." | Re-trigger Greptile

Comment thread crates/check/src/util.rs
Comment on lines 105 to +111
let start_vid = edge.start();
let end_vid = edge.end();
let forward = match prev_end {
Some(pe) if start_vid == pe && end_vid != pe => true,
Some(pe) if end_vid == pe && start_vid != pe => false,
_ => oe.is_forward(),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Connectivity-based traversal deviates from the crate's oriented-edge rule

The new wire_polygon derives start_vid = edge.start() / end_vid = edge.end() directly inside the wire iteration and uses them instead of oe.oriented_start(edge) / oe.oriented_end(edge). The check-crate rule flags exactly this pattern and points to util.rs::face_polygon as the reference. The PR intentionally replaces orientation-flag traversal with connectivity to fix the wire-chaining bug (orientation flags not chaining head-to-tail), which is a reasonable correctness fix. However, the stated rule is now violated and the rule should be updated to reflect the new canonical pattern if this approach is accepted.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/check/src/util.rs
Line: 105-111

Comment:
**Connectivity-based traversal deviates from the crate's oriented-edge rule**

The new `wire_polygon` derives `start_vid = edge.start()` / `end_vid = edge.end()` directly inside the wire iteration and uses them instead of `oe.oriented_start(edge)` / `oe.oriented_end(edge)`. The check-crate rule flags exactly this pattern and points to `util.rs::face_polygon` as the reference. The PR intentionally replaces orientation-flag traversal with connectivity to fix the wire-chaining bug (orientation flags not chaining head-to-tail), which is a reasonable correctness fix. However, the stated rule is now violated and the rule should be updated to reflect the new canonical pattern if this approach is accepted.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Intentional: on boolean-output faces the stored edge orientation flags are exactly what's unreliable here (that mis-trust was one of the three integration bugs this PR fixes), so wire_polygon chains by vertex connectivity instead. Falls back to the original ordering when chaining fails.

Comment on lines 200 to 221
@@ -180,6 +220,60 @@ pub fn perform(topo: &Topology, tol: Tolerance, arena: &mut GfaArena) -> Result<
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Secondary closed index covers Circle only, not Ellipse

The closed_index is built only for EdgeCurve::Circle boundary pave blocks and the fallback lookup likewise only matches EdgeCurve::Circle section curves. If an elliptical closed section (e.g. from a conic intersection) shares a seam mismatch with a boundary ellipse, the same duplicate-coincident-edge problem would recur. The PR description calls out circles as the target (cylinder caps), so this is a known scope, but it is worth a follow-up ticket before elliptic cases are exercised.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/algo/src/pave_filler/link_existing.rs
Line: 200-221

Comment:
**Secondary closed index covers `Circle` only, not `Ellipse`**

The `closed_index` is built only for `EdgeCurve::Circle` boundary pave blocks and the fallback lookup likewise only matches `EdgeCurve::Circle` section curves. If an elliptical closed section (e.g. from a conic intersection) shares a seam mismatch with a boundary ellipse, the same duplicate-coincident-edge problem would recur. The PR description calls out circles as the target (cylinder caps), so this is a known scope, but it is worth a follow-up ticket before elliptic cases are exercised.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, scoped to Circle deliberately — the seam-adoption gate this index backs up only fires for closed circular sections today. Extending the index to Ellipse is queued as a follow-up alongside the ellipse-section work.

andymai added 3 commits June 9, 2026 13:39
When all boundary vertices of a periodic face collapse onto the seam,
extend the span by the surface's parameter-domain period rather than a
hard-coded TAU, so periodic NURBS surfaces whose period is not 2pi
integrate correctly.
ellipses_match accepted concentric same-size ellipses rotated about the
normal as coincident. Compare the major-axis direction up to sign and
skip the check for circles, where the direction is undefined.
Compute the expected hole volume from the radius and height used to
build the cylinder so the oracle cannot desync from the geometry.
@andymai andymai enabled auto-merge (squash) June 9, 2026 20:40
@andymai andymai merged commit 3342271 into main Jun 9, 2026
18 checks passed
@andymai andymai deleted the fix/gfa-closed-section-seam-adoption branch June 9, 2026 20:43
@brepkit brepkit Bot mentioned this pull request Jun 9, 2026
andymai added a commit that referenced this pull request Jun 9, 2026
Sequential coplanar-cap cuts and cylinder-cylinder fuses regressed when
PR #755 (closed-section seam adoption) and PR #756 (periodic band
splitter) landed together.

Three coordinated fixes in the GFA builder:

- same_domain: sample wire edges along their curves instead of taking
  only the start vertex, so a circular hole left by an earlier cut is
  visible to the hole-containment test. A single-edge circle wire
  collapsed to one point and the test treated the hole as absent,
  cancelling the wall face through the hole.

- fill_images_faces: a flush coplanar cap disc (outer wire = one closed
  circle) no longer absorbs spurious IN-edge arcs projected from the
  wall outline. Those fragmented the clean disc into a many-sided
  polygon that escaped edge-set same-domain pairing and survived the cut.

- fill_images_faces: the self-boundary coincidence skip only fires when
  the partner FF face hosts the full circle within its extent. A
  cylinder rim where a narrower cylinder's cap plane slices the lateral
  must still split that lateral; skipping it produced an invalid fuse
  that wrongly passed validation.

Re-enables compound_cut_matches_sequential_2x2_grid and updates the
box-minus-cylinder golden (fewer duplicate cap vertices).
brepkit Bot added a commit that referenced this pull request Jun 9, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.102.0](v2.101.3...v2.102.0)
(2026-06-09)


### Features

* **algo:** split u-periodic faces into bands at internal section
circles ([#756](#756))
([39e9425](39e9425))


### Bug Fixes

* **algo:** adopt existing boundary vertices as seams for closed section
curves ([#755](#755))
([3342271](3342271))
* **algo:** trim plane-plane section curves to mutual face overlap
([#754](#754))
([e692c9c](e692c9c))

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

Co-authored-by: brepkit[bot] <265643962+brepkit[bot]@users.noreply.github.com>
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