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 <noreply@anthropic.com>
This commit is contained in:
Petru Conduraru
2026-07-12 19:58:49 +03:00
parent 980988f208
commit c6b15216bd
+29
View File
@@ -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")