The existing closure set (mutated in place)
The original STEP file source buffer
Full entity index with type info and byType lookup
OptionalexcludeIds: ReadonlySet<number>
Entity IDs to never add, even if they reference the
closure (e.g. pass.hiddenProductIds / a subset export's excludedIds)
Collect style and presentation-layer entities (IFCSTYLEDITEM, IFCSTYLEDREPRESENTATION, IFCPRESENTATIONLAYERASSIGNMENT, IFCPRESENTATIONLAYERWITHSTYLE) that reference geometry already in the closure, then transitively follow their own references.
In IFC STEP, IFCSTYLEDITEM references a geometry RepresentationItem, but nothing references the StyledItem back — and IFCPRESENTATIONLAYERASSIGNMENT is the same shape: it names the representation/items assigned to a layer (
AssignedItems) but nothing points back at the assignment itself. So the forward closure walk misses both entirely. This function does one reverse pass using the byType index: for each candidate, check if any referenced ID is in the closure. If yes, add it and walk its own reference chain in.Why the forward walk needs more than
excludeIdsexcludeIds(hiddenProductIds) only ever holds PRODUCT ids, never the geometry those products exclusively own.IfcPresentationLayerAssignment. AssignedItemsroutinely names REPRESENTATION-level ids directly (one CAD layer naming every wall'sIfcShapeRepresentation), so a shared layer assignment can legitimately be rescued by a VISIBLE wall's representation while also naming a HIDDEN wall's — an id nowhere inexcludeIds. A plainexcludeIds.has(id)check (the shapecollectGeoreferencingEntitiesuses) would miss that id entirely and resurrect the hidden wall's geometry.The fix relies on an invariant this function's own contract already guarantees: it runs strictly AFTER
collectReferencedEntityIds, which has already walked the FULL forward closure from every visible root. So any geometry/representation-classified entity (isGeometryEntity) that is NOT already inclosureby the time this pass starts has no other path to visibility — it is either exclusively owned by an excluded product, or genuinely unreferenced by any visible root — and must not be resurrected just because it happens to sit in the sameAssignedItems/Itemlist as something that IS visible. Presentation RESOURCE types (colours, surface styles — seeSTYLE_RESOURCE_EXEMPT_TYPES) are the one exception: they are shared data, never product-exclusive, so the pre-existing "pull in the style chain" behaviour for them is preserved unconditionally.excludeIdsis still threaded through and checked directly (matchingcollectGeoreferencingEntities's shape) as a second, cheap guard — e.g. if a future entity ever names an excluded id that is not geometry-classified.Uses byType for O(candidates) instead of O(allEntities), and byte-level scanning for #ID extraction.
Must be called AFTER collectReferencedEntityIds so the closure is complete.