From 904eb9fae88324f5d683b7e7591a7bd5e36d2c52 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 30 Oct 2024 18:08:45 +0500 Subject: [PATCH] Fix undo bugs with edited_objs Now it's stored as a collection property in the project properties, so in the most cases we delegate to Blender to ensure it's handled properly on undo and redo. --- src/bonsai/bonsai/bim/ifc.py | 29 +++++++++++++++++-- .../bonsai/bim/module/project/__init__.py | 1 + src/bonsai/bonsai/bim/module/project/prop.py | 5 ++++ src/bonsai/bonsai/tool/ifc.py | 25 ++++++++++++---- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 6d28c00d9d..c53756e753 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -45,10 +45,14 @@ class OperationData(TypedDict): obj: str +class EditObjectOperationData(TypedDict): + obj: str + + class Operation(TypedDict): rollback: Callable commit: Callable - data: Union[OperationData, None] + data: Union[OperationData, EditObjectOperationData, None] class TransactionStep(TypedDict): @@ -295,7 +299,28 @@ class IfcStore: if "guid" in data: IfcStore.guid_map[data["guid"]] = obj tool.Ifc.setup_listeners(obj) - # TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems. + + @staticmethod + def history_edit_object(obj: bpy.types.Object, *, finish_editing: bool) -> None: + if not IfcStore.history: + return + + commit, rollback = IfcStore.commit_edit_object, IfcStore.rollback_edit_object + if finish_editing: + commit, rollback = rollback, commit + + data = EditObjectOperationData(obj=obj.name) + IfcStore.history[-1]["operations"].append(Operation(rollback=rollback, commit=commit, data=data)) + + @staticmethod + def commit_edit_object(data: EditObjectOperationData) -> None: + obj = bpy.data.objects[data["obj"]] + IfcStore.edited_objs.add(obj) + + @staticmethod + def rollback_edit_object(data: EditObjectOperationData) -> None: + obj = bpy.data.objects[data["obj"]] + IfcStore.edited_objs.discard(obj) @staticmethod def rollback_unlink_element(data: OperationData) -> None: diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index 5c7e27d7cd..6f10f434c7 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -64,6 +64,7 @@ classes = ( prop.LibraryElement, prop.FilterCategory, prop.Link, + prop.EditedObj, prop.BIMProjectProperties, prop.MeasureToolSettings, ui.BIM_MT_new_project, diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index bbcebb1268..0f36faac50 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -126,6 +126,10 @@ class Link(PropertyGroup): ) +class EditedObj(PropertyGroup): + obj: PointerProperty(type=bpy.types.Object) + + class BIMProjectProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing", default=False) is_loading: BoolProperty(name="Is Loading", default=False) @@ -226,6 +230,7 @@ class BIMProjectProperties(PropertyGroup): queried_obj_root: bpy.props.PointerProperty(type=bpy.types.Object) clipping_planes: bpy.props.CollectionProperty(type=ObjProperty) clipping_planes_active: bpy.props.IntProperty(min=0, default=0, max=5) + edited_objs: bpy.props.CollectionProperty(type=EditedObj) @property def clipping_planes_objs(self): diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 521ac35a88..a8374584f4 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -176,6 +176,16 @@ class Ifc(bonsai.core.tool.Ifc): IfcStore.id_map[style.id()] = obj cls.setup_listeners(obj) + IfcStore.edited_objs = set() + edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + for i in range(len(edited_objs))[::-1]: + obj = edited_objs[i].obj + if obj: + IfcStore.edited_objs.add(obj) + else: + # Object was removed. + edited_objs.remove(i) + @classmethod def setup_listeners(cls, obj: IFC_CONNECTED_TYPE) -> None: if isinstance(obj, bpy.types.Object): @@ -200,17 +210,17 @@ class Ifc(bonsai.core.tool.Ifc): changed geometry to IFC, we mark it as changed and then it's saved later (typically during project save or switch_representation(should_sync_changes_first=True)). - Currently, underlying storage for edited objects, IfcStore.edited_objs, is not tracked - by undo system. So, it's error prone: - - undo after object was marked as edited, will keep it edited, adding unnecessary sync - - undo after object was unmarked as edited, will keep it unmarked, so edit geometry may be lost - Other caveat of using edited objects is that it won't have an effect for objects with openings, since we can't deduce non-openings representation from edited representation with openings. So, it's preferable not to use edited objects if object can have an opening. It's still can be used for spaces. """ + if obj in IfcStore.edited_objs: + return + edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + edited_objs.add().obj = obj IfcStore.edited_objs.add(obj) + IfcStore.history_edit_object(obj, finish_editing=False) @classmethod def finish_edit(cls, obj: bpy.types.Object) -> None: @@ -218,7 +228,12 @@ class Ifc(bonsai.core.tool.Ifc): Method is safe to use on an object that wasn't marked as edited before. """ + if obj not in IfcStore.edited_objs: + return + edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + edited_objs.remove(next(i for i, o in enumerate(edited_objs) if o.obj == obj)) IfcStore.edited_objs.discard(obj) + IfcStore.history_edit_object(obj, finish_editing=True) @classmethod def resolve_uri(cls, uri: str) -> str: