Bonsai: generalise representation item deletion to nested items

remove_representation_item assumed a representation item's parent is
always the IfcShapeRepresentation (or an IfcBooleanResult). Items that
live inside a container item were detached from nothing, so they stayed
in the file and were re-imported on leaving item mode (#6591).

The import side is already general. The C++ mapper tags every mapped
IfcRepresentationItem with its instance id, and container items are
mapped through the same map_to_collection helper as
IfcRepresentation.Items, so their children become individually editable
items with no special casing. Deletion now mirrors that: any inverse
that is itself a representation item is treated as a container, and the
item is cleared from whichever attribute references it. A container left
without contents is removed in turn.

This covers IfcGeometricSet/IfcGeometricCurveSet Elements,
IfcShellBasedSurfaceModel SbsmBoundary and IfcFaceBasedSurfaceModel
FbsmFaces, rather than only the case reported in #6591. Items shared by
several containers are detached from all of them, which matches item
mode presenting one object per item id.

Fixes #6591

Generated with the assistance of an AI coding tool.
This commit is contained in:
Petru Conduraru
2026-07-20 10:31:28 +03:00
parent 55a2430d71
commit 2138b0750a
+35
View File
@@ -1785,6 +1785,7 @@ class Geometry(bonsai.core.tool.Geometry):
[consider_inverses.append(texture := t) for t in getattr(representation_item, "HasTextures", [])]
representation = None
containers: list[ifcopenshell.entity_instance] = []
boolean_results_to_remove: set[ifcopenshell.entity_instance] = set()
for inverse in ifc_file.get_inverse(representation_item):
if inverse.is_a("IfcShapeRepresentation"):
@@ -1806,6 +1807,8 @@ class Geometry(bonsai.core.tool.Geometry):
elif inverse2.is_a("IfcShapeRepresentation"):
inverse2.Items = tuple(set(inverse2.Items) - {inverse} | {other_operand})
boolean_results_to_remove.add(inverse)
elif inverse.is_a("IfcRepresentationItem") and not inverse.is_a("IfcStyledItem"):
containers.append(inverse)
if styled_item:
consider_inverses.remove(styled_item)
@@ -1828,13 +1831,45 @@ class Geometry(bonsai.core.tool.Geometry):
if not new_items:
return
representation.Items = new_items
emptied_containers = [c for c in containers if cls.detach_representation_item(representation_item, c)]
also_consider = list(consider_inverses)
ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider)
for container in emptied_containers:
cls.remove_representation_item(container, element)
tool.Model.unmark_manual_booleans(element, [b.id() for b in boolean_results_to_remove])
for boolean_result in boolean_results_to_remove:
cls.remove_representation_item(boolean_result, element)
@classmethod
def detach_representation_item(
cls, representation_item: ifcopenshell.entity_instance, container: ifcopenshell.entity_instance
) -> bool:
"""Clear references to an item from another item that nests it.
Covers containers such as IfcGeometricSet.Elements,
IfcShellBasedSurfaceModel.SbsmBoundary or IfcCsgSolid.TreeRootExpression.
:param representation_item: item to detach.
:param container: item referencing ``representation_item``.
:return: whether ``container`` is left invalid and has to be removed too.
"""
is_emptied = False
for i in range(len(container)):
value = container[i]
if value == representation_item:
container[i] = None
is_emptied = True
elif isinstance(value, tuple) and any(v == representation_item for v in value):
value = tuple(v for v in value if v != representation_item)
container[i] = value
if not value:
is_emptied = True
return is_emptied
@classmethod
def create_shape_aspect(
cls,