reuse_ok_() returned true on a single-product list before ever
checking for openings, so a lone occurrence of a shared/mapped
representation with an opening applied was treated as safely
reusable. Geometry generation then took the shared/reuse path and
attributed the resulting shape to the wrong product (or the type
product, which typically has no consuming scene object), leaving
the actual occurrence with stale/empty geometry and no error or
warning logged. Fixes IfcOpenShell#9348.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Fixes #9348. When an IfcOpeningElement voids an element that is the sole occurrence of a shared/typed representation (e.g. a MappedRepresentation from an IfcElementType), the element's Body geometry silently disappeared in downstream Object-mode viewports (observed via Bonsai) — no exception, no warning/error logged, mesh ends up empty. ifcopenshell.geom.create_shape() called directly on the element returns correct, non-empty geometry with the opening properly subtracted, confirming the boolean/CSG kernel itself was never at fault.
Root cause: mapping::reuse_ok_() returned true for any single-product list before ever checking that product for openings. Whenever a caller's include= filter (or the product list naturally) narrowed down to exactly one product — the common case when editing the sole instance of a type — the size-1 fast path bypassed the find_openings() disqualification check entirely. get_representations() then treated the representation as safely shareable and processed it via the reuse/mapped path instead of computing an individual cut shape for the occurrence, and the resulting shape ended up attributed to the wrong product (or the type product, which typically has no consuming scene object downstream).
With two or more products in the list, the existing loop's find_openings() check ran as intended and correctly disqualified reuse — which is why this only manifests for singleton occurrences of a type and is easy to miss in testing (any file with ≥2 instances of the affected type masks it).
Fix
Minimal, targeted change: the products->size() == 1 fast path in reuse_ok_() now checks find_openings() on that single product before returning true, matching what the full loop would do for it anyway. Left the layerset/material checks out of the fast path deliberately, to avoid adding per-representation overhead to the (overwhelmingly common) non-shared, single-product case that has nothing to do with reuse.
Testing
AI-generated code disclosure
This change (investigation, fix, and this PR description) was produced with the assistance of an AI coding tool, reviewed and tested by the author before submission.