Skip to content

Multi-opening cuts tear thin shells open, and the kernel removes volume from a merely touching operand #3341

Description

@louistrue

Split out of #3219. The exit-cap fix in #3340 corrects the six opaque wall layers on that model. Two thin IfcCovering layers stay wrong, for a different reason, and this is that reason.

Symptom

Reporter's model (anonymized.ifc from the #3219 thread), wall #392764 / #394469. Each wall is decomposed into 4 layers; the thin covering layer of each comes out of the void cut as a torn, non-watertight shell:

mesh tris edges edges with degree != 2
part_208407_novoid 188 282 0
part_208407_cut 408 710 198
part_412191_cut 742 1432 662
part_208380_cut (opaque layer, for contrast) 392 588 0

Rank this on the bad-edge count, not on volume. signed_volume on an open shell is true volume plus T·N/3, so the figure that shows up for #208407 (about 1889) is not a volume — roughly 20.3 m2 of unbalanced vector area at a ~279 m frame offset produces it.

Why it is independent of #3340

Setting the flush-cap pad to 0.0 collapses the six opaque layers to near-zero error but leaves both coverings open and wrong (+8.31 and +102.9 abs error). So the pad is not the cause here.

Per-opening isolation on the covering at pad = 0: every single cutter behaves (worst delta -0.0397 m3), yet the cumulative 22-opening result is broken. This is an accumulation or batching failure on a thin host, not any one cutter.

Very likely the same root, reduced to a minimal case

The randomized CSG property test finds this independently on pristine main (reproduced in a clean sandbox with no source changes):

cargo test -p ifc-lite-geometry --test csg_property_test

with this seed in rust/geometry/tests/csg_property_test.proptest-regressions:

cc 0531a07bfb4e98054dbda94c642ecd9d575bd54694e83c02a8d4c556959c0174 # shrinks to pair = BoxPair { config: "touching", a_min: [-2.3397775407181403, -2.080822215875493, -6.453953867772596], a_size: [2.0898582766763107, 0.31340997700849643, 4.09485121574599], b_min: [-2.3397775407181403, -2.080822215875493, -2.359102652026606], b_size: [1.7383850838313901, 4.040893317475825, 3.241876485757496] }

A's max z and B's min z are the same f64, so the boxes touch on a face with zero-volume overlap and vol(A-B) must equal vol(A):

vol(A-B) = 1.6811517853900189
expected = 2.682055965555057

The kernel removes 1.0 m3 when subtracting a box that only touches. That is the whole defect class in one reproducer, with no IFC involved. I did not commit the regressions file, because doing so turns the suite red for every unrelated PR.

A false claim in the code, measured false

accept_cut in rust/geometry/src/router/voids/coaxial_union.rs carries:

the watertightness of the cut is guaranteed upstream: each cutter is mesh_is_closed_exact, and the kernel's conformity gate rejects a non-conforming arrangement

Measured at subtract_prisms -> subtract_mesh_many on this model, the FIRST cut:

host 188 tris (closed=true) vol 2.5913  cutters=1  ->  236 tris (closed=false) vol 2.5541

Closed in, open out, on cutter number one. The subsequent 21 cuts then run on a progressively broken shell. Adding if mesh_is_closed_exact(result) && !mesh_is_closed_exact(&cut) { return false; } to accept_cut moved #208407 a long way (1889 -> 100) without fixing it, and changed nothing else in the suite. Worth doing, not sufficient alone.

What the reference implementation does

ifcopenshell never feeds a touching cutter to its kernel. On this model it logs:

Eliminated 11 disjoint operands
Eliminated 11 touching operands

and returns identical volume with opening subtraction enabled and disabled. manifold3d independently puts the intersection volume of every (part, opening) pair at 0.0000 m3.

An operand-elimination pre-filter of that shape was prototyped during the #3219 investigation and healed both coverings on this model, but its predicate was a point sample and an adversarial pass showed it silently keeping a real 0.2 m mullion inside an opening footprint with no diagnostic. It needs a conservative predicate before it is shippable.

Suggested order

  1. Fix the touching-operand case in the kernel, using the proptest seed above as the gate. That is the root, and it needs no IFC fixture.
  2. Enforce closed-in/closed-out in accept_cut and correct its doc comment.
  3. Re-measure the two coverings on the walls/windows are still badly cut #3219 model.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrustPull requests that update rust code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions