From fcb382f46cc53728358ea0a748df8d67718a9be4 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 1 Aug 2026 09:09:33 -0500 Subject: [PATCH] Bonsai: remove_representation handles occurrence reps mapped to a floating map remove_representation took its type-level branch for any mapped representation, which no-ops when the map is not one of the element type's RepresentationMaps -- a floating/shared map (typical of Revit imports where the type's RepresentationMaps is empty). The occurrence's mapped rep was then unremovable: unassign_representation on the type finds no matching map, and remove_representation can't delete the geometry still referenced by the floating map. Take the type-level branch only when the object is the type itself, or the mapped rep is genuinely anchored to the type (new tool.Geometry.is_mapped_representation_of_type). A floating mapped rep now falls through to occurrence-level removal, which unassigns it from just that occurrence and lets remove_deep2 GC the map once its last user is gone. No change for local reps or type-anchored mapped reps. Fixes #9216. Co-Authored-By: Claude Opus 4.8 --- src/bonsai/bonsai/core/geometry.py | 10 +++++++++- src/bonsai/bonsai/core/tool.py | 1 + src/bonsai/bonsai/tool/geometry.py | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/core/geometry.py b/src/bonsai/bonsai/core/geometry.py index 83ba921d30..c33ee76d44 100644 --- a/src/bonsai/bonsai/core/geometry.py +++ b/src/bonsai/bonsai/core/geometry.py @@ -150,7 +150,15 @@ def remove_representation( element_type = geometry.get_element_type(element) data = None data_removed_by_switch_representation = False - if element_type and (geometry.is_mapped_representation(representation) or geometry.is_type_product(element)): + # Take the type-level branch only when the removal really is type-scoped: the + # object is the type itself, or the mapped rep is anchored to the type's + # RepresentationMaps. A mapped rep pointing at a *floating* map not owned by + # the type (typical of Revit imports) must be removed at the occurrence level + # instead -- the type branch would no-op and leave it unremovable. + if element_type and ( + geometry.is_type_product(element) + or geometry.is_mapped_representation_of_type(representation, element_type) + ): representation = geometry.resolve_mapped_representation(representation) data = geometry.get_representation_data(representation) if data and geometry.has_data_users(data): diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 8a77d36661..5f01b260ac 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -477,6 +477,7 @@ class Geometry: def is_box_representation(cls, representation): pass def is_data_supported_for_adding_representation(cls, data): pass def is_mapped_representation(cls, representation): pass + def is_mapped_representation_of_type(cls, representation, element_type): pass def is_type_product(cls, element): pass def link(cls, element, obj): pass def record_object_materials(cls, obj): pass diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 5747b76579..6544b18743 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1317,6 +1317,25 @@ class Geometry(bonsai.core.tool.Geometry): def is_mapped_representation(cls, representation: ifcopenshell.entity_instance) -> bool: return representation.RepresentationType == "MappedRepresentation" + @classmethod + def is_mapped_representation_of_type( + cls, + representation: ifcopenshell.entity_instance, + element_type: Union[ifcopenshell.entity_instance, None], + ) -> bool: + """``True`` when ``representation`` is a mapped representation whose + ``IfcRepresentationMap`` is one of ``element_type``'s ``RepresentationMaps`` + -- i.e. a genuine type-anchored shared representation, as opposed to a + floating map not owned by the type (which Revit exports produce).""" + if element_type is None or representation.RepresentationType != "MappedRepresentation": + return False + if not representation.Items: + return False + item = representation.Items[0] + if not item.is_a("IfcMappedItem"): + return False + return item.MappingSource in (element_type.RepresentationMaps or []) + @classmethod def is_meshlike(cls, representation: ifcopenshell.entity_instance) -> bool: if ifcopenshell.util.representation.resolve_representation(representation).RepresentationType in (