From ecde429d36d7387224b036390c1692a056429835 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 22 Mar 2026 13:53:56 +1100 Subject: [PATCH] Fix crash after undo of assign_class on macOS (#7419) After assigning an IFC class and undoing, msgbus subscriptions registered with the old Python object wrapper survived (PERSISTENT flag) but could not be cleared because: (1) rollback_link_element looked up objects by their post-link name which no longer exists after undo, and (2) the per-object clear_by_owner calls in rebuild_element_maps used new Python wrappers that didn't match the old subscription owners. Fix by using a dedicated stable object (object_subscription_owner) as the msgbus owner for all per-object subscriptions, allowing rebuild_element_maps to clear all stale subscriptions in one call regardless of Python wrapper identity changes during undo/redo. Co-Authored-By: Claude Opus 4.6 --- src/bonsai/bonsai/bim/handler.py | 13 ++++++++----- src/bonsai/bonsai/bim/ifc.py | 13 ++++--------- src/bonsai/bonsai/tool/ifc.py | 10 ++++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 231b44c671..e11eb07ce8 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -45,16 +45,19 @@ from bonsai.bim.module.nest.decorator import NestDecorator cwd = os.path.dirname(os.path.realpath(__file__)) global_subscription_owner = object() +# Separate owner for per-object msgbus subscriptions (name, active_material_index). +# Using a dedicated owner allows clearing all per-object subscriptions at once +# during undo/redo without affecting other global subscriptions. +object_subscription_owner = object() def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -> None: try: obj.name except: - # The object is invalid but somehow still has a callback. Clear all - # msgbus subscriptions to prevent useless further triggers. - bpy.msgbus.clear_by_owner(obj) - return # In case the object RNA is gone during an undo / redo operation + # The object is invalid but somehow still has a callback. + # This can occur during undo/redo when the Python wrapper is stale. + return # Blender names are up to 63 UTF-8 bytes if len(bytes(obj.name, "utf-8")) >= 63: return @@ -189,7 +192,7 @@ def subscribe_to(obj: bpy.types.ID, data_path: str, callback: Callable[[bpy.type return bpy.msgbus.subscribe_rna( key=subscribe_to, - owner=obj, + owner=object_subscription_owner, args=( obj, data_path, diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index f7d23e1dbe..b07e584a71 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -316,11 +316,8 @@ class IfcStore: del IfcStore.id_map[data["id"]] if "guid" in data: del IfcStore.guid_map[data["guid"]] - obj = IfcStore.get_object_by_name(data["obj"]) - if obj is None: - # obj was just created during this step and didn't existed before. - return - bpy.msgbus.clear_by_owner(obj) + # Note: msgbus subscriptions are cleared globally during + # rebuild_element_maps which runs after every undo/redo. @staticmethod def commit_link_element(data: OperationData) -> None: @@ -367,10 +364,8 @@ class IfcStore: del IfcStore.id_map[data["id"]] if "guid" in data: del IfcStore.guid_map[data["guid"]] - obj = IfcStore.get_object_by_name(data["obj"]) - # obj might be removed after unlink. - if not obj: - bpy.msgbus.clear_by_owner(obj) + # Note: msgbus subscriptions are cleared globally during + # rebuild_element_maps which runs after every undo/redo. @staticmethod def unlink_element( diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 8cbd2a112b..6d79ce7582 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -197,12 +197,16 @@ class Ifc(bonsai.core.tool.Ifc): if not cls.get(): return + # Clear all per-object msgbus subscriptions at once using the dedicated + # owner. After undo/redo, per-object Python wrappers have new + # identities so clearing by individual obj would miss stale + # subscriptions registered with the old wrappers. + bpy.msgbus.clear_by_owner(bonsai.bim.handler.object_subscription_owner) + for obj in bpy.data.objects: if obj.library: continue - bpy.msgbus.clear_by_owner(obj) - element = cls.get_entity(obj) if not element: continue @@ -217,8 +221,6 @@ class Ifc(bonsai.core.tool.Ifc): if obj.library: continue - bpy.msgbus.clear_by_owner(obj) - style = cls.get_entity(obj) if not style: continue