From 2138b0750a2ba5dbd59b430ca26b110b294d06ed Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Mon, 20 Jul 2026 10:31:28 +0300 Subject: [PATCH] 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. --- src/bonsai/bonsai/tool/geometry.py | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 5747b76579..269f229075 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -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,