From c6b15216bd20fc06ea2f17f7f61082aeb9c3a31b Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 12 Jul 2026 19:58:49 +0300 Subject: [PATCH] Bonsai: don't let a non-Body representation overwrite an imported wall (#8026) Some walls from a non-compliant export imported as empty objects, randomly different ones each time the file was opened. Those walls attach their Axis (Curve2D), BoundingBox and Body representations directly to the SAME IfcGeometricRepresentationContext instead of dedicated sub-contexts. Bonsai selects representations by context id, so it cannot tell them apart, and the geometry iterator yields all three shapes for the same product in one pass of create_products. The loop linked each shape unconditionally, so whichever representation was processed last won, sometimes the Body solid, sometimes the Axis curve (0 verts) or the bounding box. The order is not stable because the iterator's product set hashes on a memory address, so a different representation could win on each import, which is why the empty walls looked random. When a product already has a shape from this pass, skip any further non-Body representation, and only let a Body representation supersede an earlier one (removing the now-orphaned object and mesh). The guard is inert for well-formed models, where a product only ever yields one shape per pass. Verified live in headless Blender on the reporter's model: before the fix, 5 repeated imports produced 1 to 5 empty walls with varying membership; after, every import yields 0 empty meshes, all 4 reported walls resolve to their Body mesh, all 69 walls are valid, the linked product count is unchanged (1380), and the orphaned duplicate objects drop from 67 to 0. (This is the root-cause fix; the related PR #8444 explicitly deferred it and only addressed the multiprocessing toggle.) Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 --- src/bonsai/bonsai/bim/import_ifc.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 06f4c4afff..11e20d3b79 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -750,8 +750,37 @@ class IfcImporter: if shape: assert isinstance(shape, W.TriangulationElement) product = self.file.by_id(shape.id) + stale_obj = None + if product in results: + # Some models attach Axis/BoundingBox/Body representations + # directly to the same IfcGeometricRepresentationContext + # instead of using dedicated sub-contexts (e.g. non-compliant + # exporters). In that case a single context-ids filter can't + # tell them apart, and the iterator yields more than one + # representation for the same product in this pass. Keep + # the Body representation and ignore other (non-solid) + # duplicates for the same product, otherwise whichever + # representation happens to be processed last would + # silently replace the mesh already created for it, + # sometimes leaving the product with an empty Axis/Box + # mesh instead of its actual body geometry. See #8026. + rep_id = int(shape.geometry.id.split("-", 1)[0]) + rep = self.file.by_id(rep_id) + if getattr(rep, "RepresentationIdentifier", None) != "Body": + if not iterator.next(): + break + continue + # The Body representation is superseding a duplicate + # (non-Body) object created earlier for this same product. + # Remember it so we can clean up the now-orphaned data. + stale_obj = tool.Ifc.get_object(product) self.create_product(product, shape) results.add(product) + if stale_obj is not None and stale_obj != tool.Ifc.get_object(product): + stale_mesh = stale_obj.data if isinstance(stale_obj.data, bpy.types.Mesh) else None + bpy.data.objects.remove(stale_obj, do_unlink=True) + if stale_mesh is not None and stale_mesh.users == 0: + bpy.data.meshes.remove(stale_mesh) if not iterator.next(): break print("Done creating geometry")