Implement undo for aggregate operations, with automatic element-obj link undo/redo and improved undo syntax. See #1475.

This commit is contained in:
Dion Moult
2021-07-01 20:43:16 +10:00
parent ffbaa6f0e0
commit 3847c9bd7a
8 changed files with 143 additions and 72 deletions
+2 -2
View File
@@ -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/
+4 -1
View File
@@ -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
+50 -1
View File
@@ -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):
@@ -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"}
@@ -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"
@@ -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
@@ -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,
@@ -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"