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 <noreply@anthropic.com>
This commit is contained in:
Ryan Schultz
2026-08-01 09:09:33 -05:00
parent a11ebdf8c4
commit fcb382f46c
3 changed files with 29 additions and 1 deletions
+9 -1
View File
@@ -150,7 +150,15 @@ def remove_representation(
element_type = geometry.get_element_type(element) element_type = geometry.get_element_type(element)
data = None data = None
data_removed_by_switch_representation = False 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) representation = geometry.resolve_mapped_representation(representation)
data = geometry.get_representation_data(representation) data = geometry.get_representation_data(representation)
if data and geometry.has_data_users(data): if data and geometry.has_data_users(data):
+1
View File
@@ -477,6 +477,7 @@ class Geometry:
def is_box_representation(cls, representation): pass def is_box_representation(cls, representation): pass
def is_data_supported_for_adding_representation(cls, data): pass def is_data_supported_for_adding_representation(cls, data): pass
def is_mapped_representation(cls, representation): 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 is_type_product(cls, element): pass
def link(cls, element, obj): pass def link(cls, element, obj): pass
def record_object_materials(cls, obj): pass def record_object_materials(cls, obj): pass
+19
View File
@@ -1317,6 +1317,25 @@ class Geometry(bonsai.core.tool.Geometry):
def is_mapped_representation(cls, representation: ifcopenshell.entity_instance) -> bool: def is_mapped_representation(cls, representation: ifcopenshell.entity_instance) -> bool:
return representation.RepresentationType == "MappedRepresentation" 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 @classmethod
def is_meshlike(cls, representation: ifcopenshell.entity_instance) -> bool: def is_meshlike(cls, representation: ifcopenshell.entity_instance) -> bool:
if ifcopenshell.util.representation.resolve_representation(representation).RepresentationType in ( if ifcopenshell.util.representation.resolve_representation(representation).RepresentationType in (