Skip to content

fix(geometry): cut round window authored as two cap-to-cap extrusions#1330

Merged
louistrue merged 3 commits into
LTplus-AG:mainfrom
rpisarew:fix/issue-1320-round-window
Jun 23, 2026
Merged

fix(geometry): cut round window authored as two cap-to-cap extrusions#1330
louistrue merged 3 commits into
LTplus-AG:mainfrom
rpisarew:fix/issue-1320-round-window

Conversation

@rpisarew

@rpisarew rpisarew commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

The circular window OG-Fenster-1 (#68400) in gable wall Wand-Ext-OG-3
(#67828) of AC20-FZK-Haus.ifc does not cut through — the wall renders solid,
or with a square plug left inside the round hole. This regression surfaced
after #1320.

Reproduction

Model: AC20-FZK-Haus.ifc
(https://github.com/ibpsa/project1-wp-2-2-bim/blob/master/IFC_Files/MISC/AC20-FZK-Haus.ifc).
Load it and inspect the upper-floor gable wall 3VCarUKgH1buLo22Ozxe6J
(#67828): before the fix its round window 0e2iWM3ICUNVYAkmP5YImx (#68400) is
not cut through.

Root cause

The AC20 round window is authored as two IfcExtrudedAreaSolid cylinders
sharing the same circle profile and start point but extruding in opposite
directions
(depths 0.485 m and 0.415 m). The combined opening cutter mesh
therefore carries an interior back-to-back cap membrane mid-wall — a pair of
coincident, oppositely-wound cap disks (~28 tris) sitting inside the 0.30 m
wall thickness. The exact CSG subtract treats that double cap as a real
boundary and leaves a solid plug at the seam, so the void never cuts
through.

Fix

remove_internal_membrane, applied to the cutter before the subtract: it
buckets cap-facing cutter triangles by plane offset along the penetration axis
and deletes any interior bucket whose faces point both along and against the
axis (a glued membrane), welding the two solids into one continuous tube.

The whole interior cap plane is removed rather than only vertex-coincident
pairs — the two disks are often triangulated differently, so pair-matching
leaves a central plug (a square patch in the round hole).

  • No-op for ordinary single-solid openings (no interior back-to-back cap plane).
  • Won't merge a genuine two-hole opening — a lone interior cap points only one way.

Verification

  • New frame-independent regression wall_67828_round_window_cuts_through_native
    (thinnest-axis ray grid + interior-hole flood-fill; no guessed world
    coordinate — earlier tests falsely passed by ray-casting a point that missed
    the rotated mesh). Interior hole cells: 0 (solid) → 120 (clean disk).
  • Production submesh-path assertion added to issue_635_boolean_clipping_test.
  • Full geometry + processing suites green (394 tests); verified end-to-end
    through the WASM browser path.

Regression reported in #1320.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed opening subtraction for complex geometries where openings composed of multiple glued/extruded parts could leave an unintended internal seam plug instead of cleanly cutting through.
  • Tests

    • Added regression coverage for IFC round-window “through-hole” cases, including verification that voids propagate correctly in the production viewer path and that the cut-through behavior is detected via geometric intersection and rim/circularity checks.

The AC20 round window #68400 in gable wall #67828 is authored as two
IfcExtrudedAreaSolid cylinders sharing a profile and start point but
extruding in OPPOSITE directions. The combined cutter mesh therefore
carries an interior back-to-back cap membrane mid-wall, which the exact
CSG subtract treats as a real boundary — leaving a solid plug at the
seam so the window never cuts through.

Add remove_internal_membrane: it buckets cap-facing cutter triangles by
plane offset along the penetration axis and deletes any INTERIOR bucket
whose faces point both along and against the axis (a glued membrane),
welding the two solids into one continuous tube before the subtract. The
whole interior cap plane is removed rather than only vertex-coincident
pairs, since the two disks are often triangulated differently and
pair-matching leaves a central plug (a square patch in the round hole).
A no-op for ordinary single-solid openings, and it won't merge a genuine
two-hole opening (a lone interior cap points only one way).

Add a frame-independent regression (interior-hole flood-fill, no guessed
world coordinate) plus a production submesh-path assertion.
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@rpisarew is attempting to deploy a commit to the LTplus Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rpisarew, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 44 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db6406ab-de8e-474b-a2bb-4c0c3e9c3059

📥 Commits

Reviewing files that changed from the base of the PR and between 691b174 and cc6c3dc.

📒 Files selected for processing (1)
  • rust/geometry/src/router/voids.rs
📝 Walkthrough

Walkthrough

Adds a remove_internal_membrane helper to voids.rs that strips internal cap-plane seams from opening meshes authored as glued extrusions. The exact-kernel subtraction loop now deseams each opening before extending it through the host wall. Two regression tests (issue_1320, issue_635) verify the round window in WALL_67828 produces a clean through-hole.

Changes

Round-Window Seam Fix and Regression Tests

Layer / File(s) Summary
remove_internal_membrane helper and call site
rust/geometry/src/router/voids.rs
apply_void_context now calls Self::remove_internal_membrane(opening_mesh, depth_dir) before passing to extend_opening_mesh_through_host. The new helper buckets cap-facing triangles by axis offset, detects membrane buckets containing triangles with normals on both ±axis sides, removes those triangles while preserving outer caps, and returns the mesh unchanged if no seam is detected or input is degenerate.
Ray-casting and flood-fill verification test
rust/processing/tests/issue_1320_wall_67828_round_window.rs
Implements frame-independent through-hole verification: wall_triangles converts indexed/non-indexed meshes to world-space triangle soup; ray_crossings applies Möller–Trumbore triangle-ray intersection with numeric tolerances; interior_hole_cells builds a regular grid, casts rays along the thinnest axis, tracks crossing parity, and flood-fills to count interior hole cells; the test asserts a minimum count consistent with a clean circular through-hole.
Submesh path regression test
rust/geometry/tests/issue_635_boolean_clipping_test.rs
Exercises the "production viewer" path process_element_with_submeshes_and_voids for wall WALL_67828 / opening OPENING_68400, combines resulting submeshes into a single mesh, and validates the void cut using centre-ray hit count and rim boundary metrics (aspect ratio).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • LTplus-AG/ifc-lite#605: Both PRs modify the opening-void subtraction pipeline in rust/geometry/src/router/voids.rs by changing how opening meshes are prepared before clipper.subtract_mesh (padding extension distances vs removing an internal glued "membrane" seam).
  • LTplus-AG/ifc-lite#947: Both PRs modify rust/geometry/src/router/voids.rs's opening subtraction/CSG-void pipeline to prevent incorrect results from opening geometry (main PR by removing internal seam membranes before subtracting; retrieved PR by suppressing the AABB fallback when CSG indicates no real cut).
  • LTplus-AG/ifc-lite#1108: Both PRs target the same wall-opening seam/watertightness issues for wall WALL_67828: the main PR modifies router/voids.rs to remove internal membranes before exact-kernel subtraction, while PR #1108 modifies csg.rs::consolidate_coplanar to return the raw (watertight) mesh when consolidation introduces open edges.

Suggested labels

bug, geometry, rust

Suggested reviewers

  • louistrue

Poem

🐇 A wall had a window, a circle so round,
But a seam-plug was lurking — no hole to be found!
I bucketed triangles, I flood-filled with care,
Removed the membrane floating in there.
Now rays pass clean through, and the rim holds its shape —
This rabbit cut geometry, no seam did escape! 🕳️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately reflects the main change: fixing a geometry issue with a round window authored as two cap-to-cap extrusions, which is the core problem addressed by the pull request.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/geometry/src/router/voids.rs`:
- Around line 3724-3767: The current implementation in the buckets HashMap only
tracks whether each bucket has positive and negative facing cap faces, but does
not account for spatial separation. This causes unrelated caps from different
components that coincidentally share the same offset value (computed via the
bucket function) to be incorrectly classified as a glued membrane and removed.
Enhance the bucket structure to track not just the boolean directional flags,
but also the spatial footprint or positions of the cap faces within each bucket
(store the centroid coordinates or projected positions alongside the direction
booleans). Then when filtering for membranes, add an additional check that
verifies the positive and negative facing cap faces in the same bucket actually
overlap spatially or belong to the same connected component before marking the
bucket as removable.
- Around line 2769-2771: The deseaming operation via remove_internal_membrane is
currently only applied in the sequential subtract_mesh path before calling
extend_opening_mesh_through_host. However, the batched cutter construction path
that uses subtract_mesh_many is still passing the original opening_mesh without
deseaming. Apply the same deseaming pattern in the batch re-extension branch by
calling Self::remove_internal_membrane on opening_mesh before passing it to
extend_opening_mesh_through_host, ensuring consistent treatment across both
construction paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9247be30-8489-4161-aa5f-a2c890d0b08d

📥 Commits

Reviewing files that changed from the base of the PR and between 57261c6 and e33964f.

📒 Files selected for processing (3)
  • rust/geometry/src/router/voids.rs
  • rust/geometry/tests/issue_635_boolean_clipping_test.rs
  • rust/processing/tests/issue_1320_wall_67828_round_window.rs

Comment thread rust/geometry/src/router/voids.rs
Comment thread rust/geometry/src/router/voids.rs Outdated
rpisarew added 2 commits June 23, 2026 19:43
remove_internal_membrane keyed interior cap planes on axis-offset and
direction alone, so two laterally-separated caps that merely share an
offset (e.g. side-by-side cutters abutting at one plane) would be welded,
punching through solid material between the holes.

Track the lateral (perpendicular-to-axis) bounding box of the +facing and
-facing caps per bucket separately, treat a bucket as a membrane only
where those footprints overlap, and remove only triangles inside that
overlap region. The coincident-disk case (the real glued seam) is
unchanged; a lone cap or disjoint neighbour sharing the offset is kept.
remove_internal_membrane ran only on the sequential subtract path. The
disjoint-cutter batch path (subtract_mesh_many) extended the raw
opening_mesh at both its admission and re-extension sites, so a
two-extrusion cap-to-cap opening batched alongside another disjoint
opening kept its internal seam membrane and left a solid plug.

Apply the same deseam before extend_opening_mesh_through_host at both
batch sites. A no-op for ordinary single-solid openings, and the existing
mesh_is_closed_exact watertightness guard still gates batch admission, so
a deseamed non-watertight cutter falls through to the sequential path
(which also deseams) — no regression either way.
@louistrue louistrue merged commit d45b428 into LTplus-AG:main Jun 23, 2026
20 of 23 checks passed
ehtick pushed a commit to ehtick/ifc-lite that referenced this pull request Jun 24, 2026
…_through_host (LTplus-AG#1336)

Follow-up to LTplus-AG#1330. remove_internal_membrane was called explicitly before each of
the 3 extend_opening_mesh_through_host call sites; a future void path (or the dead
voids_2d fast path, if revived) could route an opening cutter through extend
without deseaming and silently regress the two-extrusion round-window cut.

Move the deseam to the top of extend_opening_mesh_through_host, the single helper
every void subtract funnels through, so it can't be forgotten. Pure refactor: the
3 call sites now pass the raw opening_mesh, behaviour is identical (the helper
already received the same depth_dir the explicit deseam used).

Verified: round window still cuts clean (120 interior hole cells, unchanged);
issue_635 11/11 + wall_opening_cut_regression + voids_production/submesh +
issue_964/1167/1007/960 + door_window_calibration + engulfing_solid_void all green.
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