From 3847c9bd7a52d40d38bb27a803e74bc90365b3b3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 1 Jul 2021 20:43:16 +1000 Subject: [PATCH] Implement undo for aggregate operations, with automatic element-obj link undo/redo and improved undo syntax. See #1475. --- src/blenderbim/Makefile | 4 +- src/blenderbim/blenderbim/bim/handler.py | 5 +- src/blenderbim/blenderbim/bim/ifc.py | 51 ++++++++++- .../bim/module/aggregate/operator.py | 22 ++++- .../bim/module/attribute/operator.py | 15 +--- .../bim/module/geometry/operator.py | 5 ++ .../blenderbim/bim/module/project/operator.py | 87 ++++++++++++------- .../blenderbim/bim/module/root/operator.py | 26 ++---- 8 files changed, 143 insertions(+), 72 deletions(-) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 23aa47d9f7..70af090dda 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -13,10 +13,10 @@ endif # Provides IfcOpenShell Python functionality ifeq ($(PYVERSION), py37) - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-81ad689-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-37-v0.6.0-2fd2b49-$(PLATFORM)64.zip endif ifeq ($(PYVERSION), py39) - cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-81ad689-$(PLATFORM)64.zip + cd dist/working && wget https://s3.amazonaws.com/ifcopenshell-builds/ifcblender-python-39-v0.6.0-2fd2b49-$(PLATFORM)64.zip endif cd dist/working && unzip ifcblender* cp -r dist/working/io_import_scene_ifc/ifcopenshell dist/blenderbim/libs/site/packages/ diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index d9f27dd3b9..28eb81fcd0 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -12,7 +12,10 @@ global_subscription_owner = object() def mode_callback(obj, data): - for obj in bpy.context.selected_objects + [bpy.context.active_object]: + objects = bpy.context.selected_objects + if bpy.context.active_object: + objects += [bpy.context.active_object] + for obj in objects: if ( obj.mode != "EDIT" or not obj.data diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index d35b6e2d75..fee0af8f7f 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -75,7 +75,9 @@ class IfcStore: if not file: return if should_reload_selected: - objects = bpy.context.selected_objects + [bpy.context.active_object] + objects = bpy.context.selected_objects + if bpy.context.active_object: + objects += [bpy.context.active_object] else: objects = bpy.data.objects [ @@ -95,6 +97,24 @@ class IfcStore: for listener in IfcStore.element_listeners: listener(element, obj) + if IfcStore.history: + data = {"id": element.id(), "guid": getattr(element, "GlobalId", None), "obj": obj.name} + IfcStore.history[-1]["transactions"].append( + {"rollback": IfcStore.rollback_link_element, "commit": IfcStore.commit_link_element, "data": data} + ) + + @staticmethod + def rollback_link_element(data): + del IfcStore.id_map[data["id"]] + if data["guid"]: + del IfcStore.guid_map[data["guid"]] + + @staticmethod + def commit_link_element(data): + IfcStore.id_map[data["id"]] = bpy.data.objects.get(data["obj"]) + if data["guid"]: + IfcStore.guid_map[data["guid"]] = bpy.data.objects.get(data["obj"]) + @staticmethod def unlink_element(element=None, obj=None): if element is None: @@ -120,6 +140,35 @@ class IfcStore: if obj: obj.BIMObjectProperties.ifc_definition_id = 0 + @staticmethod + def execute_ifc_operator(operator, context): + is_top_level_operator = not bool(operator.transaction_key) + + if is_top_level_operator: + IfcStore.get_file().begin_transaction() + # This empty transaction ensures that each operator has at least one transaction + IfcStore.add_transaction(operator, rollback=lambda data: True, commit=lambda data: True) + + result = getattr(operator, "_execute")(context) + + if is_top_level_operator: + IfcStore.get_file().end_transaction() + IfcStore.add_transaction( + operator, rollback=IfcStore.rollback_ifc_operator, commit=IfcStore.commit_ifc_operator + ) + + return result + + @staticmethod + def rollback_ifc_operator(data): + IfcStore.get_file().undo() + blenderbim.bim.handler.purge_module_data() + + @staticmethod + def commit_ifc_operator(data): + IfcStore.get_file().redo() + blenderbim.bim.handler.purge_module_data() + @staticmethod def generate_transaction_key(operator): if not getattr(operator, "transaction_key", None): diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index 3b8aa1624a..941210aaa6 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -7,10 +7,15 @@ from ifcopenshell.api.aggregate.data import Data class AssignObject(bpy.types.Operator): bl_idname = "bim.assign_object" bl_label = "Assign Object" + bl_options = {"REGISTER", "UNDO"} + transaction_key: bpy.props.StringProperty() relating_object: bpy.props.StringProperty() related_object: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() related_objects = ( [bpy.data.objects.get(self.related_object)] if self.related_object else bpy.context.selected_objects @@ -29,7 +34,7 @@ class AssignObject(bpy.types.Operator): "relating_object": self.file.by_id(relating_object.BIMObjectProperties.ifc_definition_id), }, ) - bpy.ops.bim.edit_object_placement(obj=related_object.name) + bpy.ops.bim.edit_object_placement(transaction_key=self.transaction_key, obj=related_object.name) Data.load(IfcStore.get_file(), oprops.ifc_definition_id) bpy.ops.bim.disable_editing_aggregate(obj=related_object.name) @@ -60,6 +65,7 @@ class AssignObject(bpy.types.Operator): class EnableEditingAggregate(bpy.types.Operator): bl_idname = "bim.enable_editing_aggregate" bl_label = "Enable Editing Aggregate" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bpy.context.active_object.BIMObjectProperties.relating_object = None @@ -71,6 +77,7 @@ class DisableEditingAggregate(bpy.types.Operator): bl_idname = "bim.disable_editing_aggregate" bl_label = "Disable Editing Aggregate" obj: bpy.props.StringProperty() + bl_options = {"REGISTER", "UNDO"} def execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object @@ -81,14 +88,23 @@ class DisableEditingAggregate(bpy.types.Operator): class AddAggregate(bpy.types.Operator): bl_idname = "bim.add_aggregate" bl_label = "Add Aggregate" + bl_options = {"REGISTER", "UNDO"} + transaction_key: bpy.props.StringProperty() obj: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object aggregate_collection = bpy.data.collections.new("IfcElementAssembly/Assembly") bpy.context.scene.collection.children.link(aggregate_collection) aggregate = bpy.data.objects.new("Assembly", None) aggregate_collection.objects.link(aggregate) - bpy.ops.bim.assign_class(obj=aggregate.name, ifc_class="IfcElementAssembly") - bpy.ops.bim.assign_object(related_object=obj.name, relating_object=aggregate.name) + bpy.ops.bim.assign_class( + transaction_key=self.transaction_key, obj=aggregate.name, ifc_class="IfcElementAssembly" + ) + bpy.ops.bim.assign_object( + transaction_key=self.transaction_key, related_object=obj.name, relating_object=aggregate.name + ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/attribute/operator.py b/src/blenderbim/blenderbim/bim/module/attribute/operator.py index d52632d93f..9d05731d3c 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/operator.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/operator.py @@ -69,6 +69,9 @@ class EditAttributes(bpy.types.Operator): obj_type: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() if self.obj_type == "Object": obj = bpy.data.objects.get(self.obj) @@ -99,11 +102,9 @@ class EditAttributes(bpy.types.Operator): elif attribute["type"] == "enum": attributes[attribute["name"]] = blender_attribute.enum_value product = self.file.by_id(oprops.ifc_definition_id) - self.file.begin_transaction() ifcopenshell.api.run( "attribute.edit_attributes", self.file, **{"product": product, "attributes": attributes} ) - self.file.end_transaction() if "Name" in attributes: new_name = "{}/{}".format(product.is_a(), product.Name or "Unnamed") collection = bpy.data.collections.get(obj.name) @@ -112,18 +113,8 @@ class EditAttributes(bpy.types.Operator): obj.name = new_name Data.load(IfcStore.get_file(), oprops.ifc_definition_id) bpy.ops.bim.disable_editing_attributes(obj=obj.name, obj_type=self.obj_type) - self.transaction_data = {"ifc_definition_id": oprops.ifc_definition_id} - IfcStore.add_transaction(self) return {"FINISHED"} - def rollback(self, data): - IfcStore.get_file().undo() - Data.load(IfcStore.get_file(), data["ifc_definition_id"]) - - def commit(self, data): - IfcStore.get_file().redo() - Data.load(IfcStore.get_file(), data["ifc_definition_id"]) - class GenerateGlobalId(bpy.types.Operator): bl_idname = "bim.generate_global_id" diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index e43860125b..0e1dbb1c29 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -17,9 +17,14 @@ from mathutils import Vector class EditObjectPlacement(bpy.types.Operator): bl_idname = "bim.edit_object_placement" bl_label = "Edit Object Placement" + bl_options = {"REGISTER", "UNDO"} + transaction_key: bpy.props.StringProperty() obj: bpy.props.StringProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): objs = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects self.file = IfcStore.get_file() # TODO: determine how to deal with this module dependency diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 48e0d2e082..f63ae42cef 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -16,6 +16,14 @@ class CreateProject(bpy.types.Operator): transaction_key: bpy.props.StringProperty() def execute(self, context): + IfcStore.generate_transaction_key(self) + IfcStore.add_transaction(self, rollback=self.rollback, commit=lambda data: True) + result = self._execute(context) + self.transaction_data = {"file": self.file} + IfcStore.add_transaction(self, rollback=lambda data: True, commit=self.commit) + return result + + def _execute(self, context): self.file = IfcStore.get_file() if self.file: return {"FINISHED"} @@ -25,9 +33,6 @@ class CreateProject(bpy.types.Operator): ) self.file = IfcStore.get_file() - self.transaction_data = {"file": self.file} - IfcStore.add_transaction(self) - bpy.ops.bim.add_person() bpy.ops.bim.add_organisation() @@ -53,9 +58,9 @@ class CreateProject(bpy.types.Operator): bpy.ops.bim.assign_class( transaction_key=self.transaction_key, obj=building_storey.name, ifc_class="IfcBuildingStorey" ) - bpy.ops.bim.assign_object(related_object=site.name, relating_object=project.name) - bpy.ops.bim.assign_object(related_object=building.name, relating_object=site.name) - bpy.ops.bim.assign_object(related_object=building_storey.name, relating_object=building.name) + bpy.ops.bim.assign_object(transaction_key=self.transaction_key, related_object=site.name, relating_object=project.name) + bpy.ops.bim.assign_object(transaction_key=self.transaction_key, related_object=building.name, relating_object=site.name) + bpy.ops.bim.assign_object(transaction_key=self.transaction_key, related_object=building_storey.name, relating_object=building.name) return {"FINISHED"} @@ -75,6 +80,14 @@ class CreateProjectLibrary(bpy.types.Operator): transaction_key: bpy.props.StringProperty() def execute(self, context): + IfcStore.generate_transaction_key(self) + IfcStore.add_transaction(self, rollback=self.rollback, commit=lambda data: True) + result = self._execute(context) + self.transaction_data = {"file": self.file} + IfcStore.add_transaction(self, rollback=lambda data: True, commit=self.commit) + return result + + def _execute(self, context): self.file = IfcStore.get_file() if self.file: return {"FINISHED"} @@ -84,9 +97,6 @@ class CreateProjectLibrary(bpy.types.Operator): ) self.file = IfcStore.get_file() - self.transaction_data = {"file": self.file} - IfcStore.add_transaction(self) - if self.file.schema == "IFC2X3": bpy.ops.bim.add_person() bpy.ops.bim.add_organisation() @@ -116,12 +126,15 @@ class SelectLibraryFile(bpy.types.Operator): def execute(self, context): old_filepath = IfcStore.library_path + result = self._execute(context) + self.transaction_data = {"old_filepath": old_filepath, "filepath": self.filepath} + IfcStore.add_transaction(self) + return result + + def _execute(self, context): IfcStore.library_path = self.filepath IfcStore.library_file = ifcopenshell.open(self.filepath) bpy.ops.bim.refresh_library() - - self.transaction_data = {"old_filepath": old_filepath, "filepath": self.filepath} - IfcStore.add_transaction(self) return {"FINISHED"} def invoke(self, context, event): @@ -217,23 +230,28 @@ class AssignLibraryDeclaration(bpy.types.Operator): bl_idname = "bim.assign_library_declaration" bl_label = "Assign Library Declaration" bl_options = {"REGISTER", "UNDO"} + transaction_key: bpy.props.StringProperty() definition: bpy.props.IntProperty() def execute(self, context): + IfcStore.library_file.begin_transaction() + result = self._execute(context) + IfcStore.library_file.end_transaction() + IfcStore.add_transaction(self) + return result + + def _execute(self, context): self.props = context.scene.BIMProjectProperties self.file = IfcStore.library_file - self.file.begin_transaction() ifcopenshell.api.run( "project.assign_declaration", self.file, definition=self.file.by_id(self.definition), relating_context=self.file.by_type("IfcProjectLibrary")[0], ) - self.file.end_transaction() element_name = self.props.active_library_element bpy.ops.bim.rewind_library() bpy.ops.bim.change_library_element(element_name=element_name) - IfcStore.add_transaction(self) return {"FINISHED"} def rollback(self, data): @@ -247,23 +265,28 @@ class UnassignLibraryDeclaration(bpy.types.Operator): bl_idname = "bim.unassign_library_declaration" bl_label = "Unassign Library Declaration" bl_options = {"REGISTER", "UNDO"} + transaction_key: bpy.props.StringProperty() definition: bpy.props.IntProperty() def execute(self, context): + IfcStore.library_file.begin_transaction() + result = self._execute(context) + IfcStore.library_file.end_transaction() + IfcStore.add_transaction(self) + return result + + def _execute(self, context): self.props = context.scene.BIMProjectProperties self.file = IfcStore.library_file - self.file.begin_transaction() ifcopenshell.api.run( "project.unassign_declaration", self.file, definition=self.file.by_id(self.definition), relating_context=self.file.by_type("IfcProjectLibrary")[0], ) - self.file.end_transaction() element_name = self.props.active_library_element bpy.ops.bim.rewind_library() bpy.ops.bim.change_library_element(element_name=element_name) - IfcStore.add_transaction(self) return {"FINISHED"} def rollback(self, data): @@ -286,21 +309,22 @@ class AppendLibraryElement(bpy.types.Operator): bl_idname = "bim.append_library_element" bl_label = "Append Library Element" bl_options = {"REGISTER", "UNDO"} + transaction_key: bpy.props.StringProperty() definition: bpy.props.IntProperty() def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): self.file = IfcStore.get_file() - self.file.begin_transaction() element = ifcopenshell.api.run( "project.append_asset", self.file, library=IfcStore.library_file, element=IfcStore.library_file.by_id(self.definition), ) - self.file.end_transaction() self.import_type_from_ifc(element) blenderbim.bim.handler.purge_module_data() - IfcStore.add_transaction(self) return {"FINISHED"} def import_type_from_ifc(self, element): @@ -322,12 +346,6 @@ class AppendLibraryElement(bpy.types.Operator): ifc_importer.create_type_product(element) ifc_importer.place_objects_in_spatial_tree() - def rollback(self, data): - IfcStore.get_file().undo() - - def commit(self, data): - IfcStore.get_file().redo() - class EnableEditingHeader(bpy.types.Operator): bl_idname = "bim.enable_editing_header" @@ -367,24 +385,27 @@ class EditHeader(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): + self.transaction_data = {} + self.transaction_data["old"] = self.record_state() + result = self._execute(context) + self.transaction_data["new"] = self.record_state() + IfcStore.add_transaction(self) + return result + + def _execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMProjectProperties props.is_editing = True - self.transaction_data = {} - self.transaction_data["old"] = self.record_state() - self.file.wrapped_data.header.file_description.description = (f"ViewDefinition[{props.mvd}]",) self.file.wrapped_data.header.file_name.author = (props.author_name, props.author_email) self.file.wrapped_data.header.file_name.organization = (props.organisation_name, props.organisation_email) self.file.wrapped_data.header.file_name.authorization = props.authorisation bpy.ops.bim.disable_editing_header() - - self.transaction_data["new"] = self.record_state() - IfcStore.add_transaction(self) return {"FINISHED"} def record_state(self): + self.file = IfcStore.get_file() return { "description": self.file.wrapped_data.header.file_description.description, "author": self.file.wrapped_data.header.file_name.author, diff --git a/src/blenderbim/blenderbim/bim/module/root/operator.py b/src/blenderbim/blenderbim/bim/module/root/operator.py index 11cdfa2c78..9797a88dc6 100644 --- a/src/blenderbim/blenderbim/bim/module/root/operator.py +++ b/src/blenderbim/blenderbim/bim/module/root/operator.py @@ -88,7 +88,9 @@ class AssignClass(bpy.types.Operator): ifc_representation_class: bpy.props.StringProperty() def execute(self, context): - self.transaction_data = [] + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): objects = [bpy.data.objects.get(self.obj)] if self.obj else bpy.context.selected_objects self.file = IfcStore.get_file() self.declaration = IfcStore.get_schema().declaration_by_name(self.ifc_class) @@ -98,13 +100,11 @@ class AssignClass(bpy.types.Operator): predefined_type = None for obj in objects: self.assign_class(context, obj) - IfcStore.add_transaction(self) return {"FINISHED"} def assign_class(self, context, obj): if obj.BIMObjectProperties.ifc_definition_id: return - self.file.begin_transaction() product = ifcopenshell.api.run( "root.create_entity", self.file, @@ -114,10 +114,8 @@ class AssignClass(bpy.types.Operator): "name": obj.name, }, ) - self.file.end_transaction() obj.name = "{}/{}".format(product.is_a(), obj.name) IfcStore.link_element(product, obj) - self.transaction_data.append({"element": product.id(), "obj": obj.name}) if self.should_add_representation: bpy.ops.bim.add_representation( @@ -162,7 +160,9 @@ class AssignClass(bpy.types.Operator): collection.objects.link(obj) if parent_collection: parent_collection.children.link(collection) - bpy.ops.bim.assign_object(related_object=obj.name, relating_object=parent_collection.name) + bpy.ops.bim.assign_object( + transaction_key=self.transaction_key, related_object=obj.name, relating_object=parent_collection.name + ) else: bpy.context.scene.collection.children.link(collection) @@ -177,20 +177,6 @@ class AssignClass(bpy.types.Operator): ) break - def rollback(self, data): - for linked_element in data: - IfcStore.unlink_element( - IfcStore.get_file().by_id(linked_element["element"]), bpy.data.objects.get(linked_element["obj"]) - ) - IfcStore.get_file().undo() - - def commit(self, data): - IfcStore.get_file().redo() - for linked_element in data: - IfcStore.link_element( - IfcStore.get_file().by_id(linked_element["element"]), bpy.data.objects.get(linked_element["obj"]) - ) - class UnassignClass(bpy.types.Operator): bl_idname = "bim.unassign_class"