From b1d731447ab362dfe4e365566255d004ee0cb84e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 6 Jul 2021 15:06:21 +1000 Subject: [PATCH] Implement undo for non-module operators. See #1475. --- src/blenderbim/blenderbim/bim/__init__.py | 5 - .../blenderbim/bim/module/attribute/ui.py | 4 - src/blenderbim/blenderbim/bim/operator.py | 271 ++++++++++-------- src/ifcopenshell-python/ifcopenshell/file.py | 12 +- 4 files changed, 158 insertions(+), 134 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 4e49f4466e..6f1531121c 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -61,9 +61,6 @@ if bpy is not None: operator.SelectIfcFile, operator.ExportIFC, operator.ImportIFC, - operator.SelectExternalMaterialDir, - operator.FetchExternalMaterial, - operator.FetchObjectPassport, operator.OpenUpstream, operator.AddSectionPlane, operator.RemoveSectionPlane, @@ -72,8 +69,6 @@ if bpy is not None: operator.RemoveIfcFile, operator.SetOverrideColour, operator.SetViewportShadowFromSun, - operator.CopyPropertyToSelection, - operator.CopyAttributeToSelection, operator.LinkIfc, operator.SnapSpacesTogether, prop.StrProperty, diff --git a/src/blenderbim/blenderbim/bim/module/attribute/ui.py b/src/blenderbim/blenderbim/bim/module/attribute/ui.py index 2a12aa343d..68573cb3ee 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/ui.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/ui.py @@ -42,10 +42,6 @@ def draw_ui(context, layout, obj_type): icon="RADIOBUT_OFF" if blender_attribute.is_null else "RADIOBUT_ON", text="", ) - # TODO: reimplement, see #1222 - # op = row.operator("bim.copy_attribute_to_selection", icon="COPYDOWN", text="") - # op.attribute_name = attribute.name - # op.attribute_value = attribute.string_value else: row = layout.row() op = row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit") diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 18eee614e7..ccf51aee5d 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -20,6 +20,7 @@ from math import radians, degrees, atan, tan, cos, sin class ExportIFC(bpy.types.Operator): bl_idname = "export_ifc.bim" bl_label = "Export IFC" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -40,10 +41,15 @@ class ExportIFC(bpy.types.Operator): return {"RUNNING_MODAL"} def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): start = time.time() logger = logging.getLogger("ExportIFC") logging.basicConfig( - filename=os.path.join(context.scene.BIMProperties.data_dir, "process.log"), filemode="a", level=logging.DEBUG + filename=os.path.join(context.scene.BIMProperties.data_dir, "process.log"), + filemode="a", + level=logging.DEBUG, ) extension = self.filepath.split(".")[-1] if extension == "ifczip": @@ -75,6 +81,7 @@ class ExportIFC(bpy.types.Operator): class ImportIFC(bpy.types.Operator, ImportHelper): bl_idname = "import_ifc.bim" bl_label = "Import IFC" + bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) @@ -102,7 +109,9 @@ class ImportIFC(bpy.types.Operator, ImportHelper): start = time.time() logger = logging.getLogger("ImportIFC") logging.basicConfig( - filename=os.path.join(bpy.context.scene.BIMProperties.data_dir, "process.log"), filemode="a", level=logging.DEBUG + filename=os.path.join(bpy.context.scene.BIMProperties.data_dir, "process.log"), + filemode="a", + level=logging.DEBUG, ) settings = import_ifc.IfcImportSettings.factory(context, self.filepath, logger) @@ -139,23 +148,10 @@ class OpenUri(bpy.types.Operator): return {"FINISHED"} -class SelectExternalMaterialDir(bpy.types.Operator): - bl_idname = "bim.select_external_material_dir" - bl_label = "Select Material File" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - bpy.context.active_object.active_material.BIMMaterialProperties.location = self.filepath - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - class SelectIfcFile(bpy.types.Operator): bl_idname = "bim.select_ifc_file" bl_label = "Select IFC File" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) @@ -171,6 +167,7 @@ class SelectIfcFile(bpy.types.Operator): class SelectDataDir(bpy.types.Operator): bl_idname = "bim.select_data_dir" bl_label = "Select Data Directory" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): @@ -185,6 +182,7 @@ class SelectDataDir(bpy.types.Operator): class SelectSchemaDir(bpy.types.Operator): bl_idname = "bim.select_schema_dir" bl_label = "Select Schema Directory" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): @@ -196,52 +194,6 @@ class SelectSchemaDir(bpy.types.Operator): return {"RUNNING_MODAL"} -class FetchExternalMaterial(bpy.types.Operator): - bl_idname = "bim.fetch_external_material" - bl_label = "Fetch External Material" - - def execute(self, context): - location = bpy.context.active_object.active_material.BIMMaterialProperties.location - if location[-6:] != ".mpass": - return {"FINISHED"} - if not os.path.isabs(location): - location = os.path.join(bpy.context.scene.BIMProperties.data_dir, location) - with open(location) as f: - self.material_pass = json.load(f) - if bpy.context.scene.render.engine == "BLENDER_EEVEE" and "eevee" in self.material_pass: - self.fetch_eevee_or_cycles("eevee") - elif bpy.context.scene.render.engine == "CYCLES" and "cycles" in self.material_pass: - self.fetch_eevee_or_cycles("cycles") - return {"FINISHED"} - - def fetch_eevee_or_cycles(self, name): - identification = bpy.context.active_object.active_material.BIMMaterialProperties.identification - uri = self.material_pass[name]["uri"] - if not os.path.isabs(uri): - uri = os.path.join(bpy.context.scene.BIMProperties.data_dir, uri) - bpy.ops.wm.link(filename=identification, directory=os.path.join(uri, "Material")) - for material in bpy.data.materials: - if material.name == identification and material.library: - bpy.context.active_object.material_slots[0].material = material - return - - -class FetchObjectPassport(bpy.types.Operator): - bl_idname = "bim.fetch_object_passport" - bl_label = "Fetch Object Passport" - - def execute(self, context): - for reference in bpy.context.active_object.BIMObjectProperties.document_references: - reference = bpy.context.scene.BIMProperties.document_references[reference.name] - if reference.location[-6:] == ".blend": - self.fetch_blender(reference) - return {"FINISHED"} - - def fetch_blender(self, reference): - bpy.ops.wm.link(filename=reference.name, directory=os.path.join(reference.location, "Mesh")) - bpy.context.active_object.data = bpy.data.meshes[reference.name] - - class OpenUpstream(bpy.types.Operator): bl_idname = "bim.open_upstream" bl_label = "Open Upstream Reference" @@ -259,70 +211,10 @@ class OpenUpstream(bpy.types.Operator): return {"FINISHED"} -class CopyPropertyToSelection(bpy.types.Operator): - bl_idname = "bim.copy_property_to_selection" - bl_label = "Copy Property To Selection" - pset_name: bpy.props.StringProperty() - prop_name: bpy.props.StringProperty() - prop_value: bpy.props.StringProperty() - - def execute(self, context): - for obj in bpy.context.selected_objects: - if "/" not in obj.name: - continue - pset = obj.BIMObjectProperties.psets.get(self.pset_name) - if not pset: - applicable_psets = schema.ifc.psetqto.get_applicable(obj.name.split("/")[0], pset_only=True) - for pset_template in applicable_psets: - if pset_template.Name == self.pset_name: - break - else: - continue - pset = obj.BIMObjectProperties.psets.add() - pset.name = self.pset_name - for template_prop_name in (p.Name for p in pset_template.HasPropertyTemplates): - prop = pset.properties.add() - prop.name = template_prop_name - prop = pset.properties.get(self.prop_name) - if prop: - prop.string_value = self.prop_value - return {"FINISHED"} - - -class CopyAttributeToSelection(bpy.types.Operator): - bl_idname = "bim.copy_attribute_to_selection" - bl_label = "Copy Attribute To Selection" - attribute_name: bpy.props.StringProperty() - attribute_value: bpy.props.StringProperty() - - def execute(self, context): - # TODO: reimplement - self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(bpy.context.scene.BIMProperties.export_schema) - self.applicable_attributes_cache = {} - for obj in bpy.context.selected_objects: - if "/" not in obj.name: - continue - attribute = obj.BIMObjectProperties.attributes.get(self.attribute_name) - if not attribute: - applicable_attributes = self.get_applicable_attributes(obj.name.split("/")[0]) - if self.attribute_name not in applicable_attributes: - continue - attribute = obj.BIMObjectProperties.attributes.add() - attribute.name = self.attribute_name - attribute.string_value = self.attribute_value - return {"FINISHED"} - - def get_applicable_attributes(self, ifc_class): - if ifc_class not in self.applicable_attributes_cache: - self.applicable_attributes_cache[ifc_class] = [ - a.name() for a in self.schema.declaration_by_name(ifc_class).all_attributes() - ] - return self.applicable_attributes_cache[ifc_class] - - class AddSectionPlane(bpy.types.Operator): bl_idname = "bim.add_section_plane" bl_label = "Add Temporary Section Cutaway" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): obj = self.create_section_obj() @@ -490,6 +382,7 @@ class AddSectionPlane(bpy.types.Operator): class RemoveSectionPlane(bpy.types.Operator): bl_idname = "bim.remove_section_plane" bl_label = "Remove Temporary Section Cutaway" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): name = bpy.context.active_object.name @@ -544,6 +437,7 @@ class RemoveSectionPlane(bpy.types.Operator): class ReloadIfcFile(bpy.types.Operator): bl_idname = "bim.reload_ifc_file" bl_label = "Reload IFC File" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): # TODO: reimplement. See #1222. @@ -553,6 +447,7 @@ class ReloadIfcFile(bpy.types.Operator): class AddIfcFile(bpy.types.Operator): bl_idname = "bim.add_ifc_file" bl_label = "Add IFC File" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bpy.context.scene.DocProperties.ifc_files.add() @@ -563,6 +458,7 @@ class RemoveIfcFile(bpy.types.Operator): bl_idname = "bim.remove_ifc_file" bl_label = "Remove IFC File" index: bpy.props.IntProperty() + bl_options = {"REGISTER", "UNDO"} def execute(self, context): bpy.context.scene.DocProperties.ifc_files.remove(self.index) @@ -572,6 +468,7 @@ class RemoveIfcFile(bpy.types.Operator): class SetOverrideColour(bpy.types.Operator): bl_idname = "bim.set_override_colour" bl_label = "Set Override Colour" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): result = 0 @@ -585,6 +482,7 @@ class SetOverrideColour(bpy.types.Operator): class SetViewportShadowFromSun(bpy.types.Operator): bl_idname = "bim.set_viewport_shadow_from_sun" bl_label = "Set Viewport Shadow from Sun" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): # The vector used for the light direction is a bit funny @@ -598,6 +496,7 @@ class SetViewportShadowFromSun(bpy.types.Operator): class LinkIfc(bpy.types.Operator): bl_idname = "bim.link_ifc" bl_label = "Link IFC" + bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): @@ -625,6 +524,7 @@ class LinkIfc(bpy.types.Operator): class SnapSpacesTogether(bpy.types.Operator): bl_idname = "bim.snap_spaces_together" bl_label = "Snap Spaces Together" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): threshold = 0.5 @@ -662,3 +562,128 @@ class SnapSpacesTogether(bpy.types.Operator): for v in polygon.vertices: obj.data.vertices[v].co += offset return {"FINISHED"} + + +class SelectExternalMaterialDir(bpy.types.Operator): + bl_idname = "bim.select_external_material_dir" + bl_label = "Select Material File" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + bpy.context.active_object.active_material.BIMMaterialProperties.location = self.filepath + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class FetchExternalMaterial(bpy.types.Operator): + bl_idname = "bim.fetch_external_material" + bl_label = "Fetch External Material" + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + location = bpy.context.active_object.active_material.BIMMaterialProperties.location + if location[-6:] != ".mpass": + return {"FINISHED"} + if not os.path.isabs(location): + location = os.path.join(bpy.context.scene.BIMProperties.data_dir, location) + with open(location) as f: + self.material_pass = json.load(f) + if bpy.context.scene.render.engine == "BLENDER_EEVEE" and "eevee" in self.material_pass: + self.fetch_eevee_or_cycles("eevee") + elif bpy.context.scene.render.engine == "CYCLES" and "cycles" in self.material_pass: + self.fetch_eevee_or_cycles("cycles") + return {"FINISHED"} + + def fetch_eevee_or_cycles(self, name): + identification = bpy.context.active_object.active_material.BIMMaterialProperties.identification + uri = self.material_pass[name]["uri"] + if not os.path.isabs(uri): + uri = os.path.join(bpy.context.scene.BIMProperties.data_dir, uri) + bpy.ops.wm.link(filename=identification, directory=os.path.join(uri, "Material")) + for material in bpy.data.materials: + if material.name == identification and material.library: + bpy.context.active_object.material_slots[0].material = material + return + + +class FetchObjectPassport(bpy.types.Operator): + bl_idname = "bim.fetch_object_passport" + bl_label = "Fetch Object Passport" + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + for reference in bpy.context.active_object.BIMObjectProperties.document_references: + reference = bpy.context.scene.BIMProperties.document_references[reference.name] + if reference.location[-6:] == ".blend": + self.fetch_blender(reference) + return {"FINISHED"} + + def fetch_blender(self, reference): + bpy.ops.wm.link(filename=reference.name, directory=os.path.join(reference.location, "Mesh")) + bpy.context.active_object.data = bpy.data.meshes[reference.name] + + +class CopyPropertyToSelection(bpy.types.Operator): + bl_idname = "bim.copy_property_to_selection" + bl_label = "Copy Property To Selection" + pset_name: bpy.props.StringProperty() + prop_name: bpy.props.StringProperty() + prop_value: bpy.props.StringProperty() + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + for obj in bpy.context.selected_objects: + if "/" not in obj.name: + continue + pset = obj.BIMObjectProperties.psets.get(self.pset_name) + if not pset: + applicable_psets = schema.ifc.psetqto.get_applicable(obj.name.split("/")[0], pset_only=True) + for pset_template in applicable_psets: + if pset_template.Name == self.pset_name: + break + else: + continue + pset = obj.BIMObjectProperties.psets.add() + pset.name = self.pset_name + for template_prop_name in (p.Name for p in pset_template.HasPropertyTemplates): + prop = pset.properties.add() + prop.name = template_prop_name + prop = pset.properties.get(self.prop_name) + if prop: + prop.string_value = self.prop_value + return {"FINISHED"} + + +class CopyAttributeToSelection(bpy.types.Operator): + bl_idname = "bim.copy_attribute_to_selection" + bl_label = "Copy Attribute To Selection" + attribute_name: bpy.props.StringProperty() + attribute_value: bpy.props.StringProperty() + + def execute(self, context): + # TODO: this is dead code, awaiting reimplementation. See #1222. + self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(bpy.context.scene.BIMProperties.export_schema) + self.applicable_attributes_cache = {} + for obj in bpy.context.selected_objects: + if "/" not in obj.name: + continue + attribute = obj.BIMObjectProperties.attributes.get(self.attribute_name) + if not attribute: + applicable_attributes = self.get_applicable_attributes(obj.name.split("/")[0]) + if self.attribute_name not in applicable_attributes: + continue + attribute = obj.BIMObjectProperties.attributes.add() + attribute.name = self.attribute_name + attribute.string_value = self.attribute_value + return {"FINISHED"} + + def get_applicable_attributes(self, ifc_class): + if ifc_class not in self.applicable_attributes_cache: + self.applicable_attributes_cache[ifc_class] = [ + a.name() for a in self.schema.declaration_by_name(ifc_class).all_attributes() + ] + return self.applicable_attributes_cache[ifc_class] diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index 9a5b927987..2963cf662b 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -51,10 +51,18 @@ class Transaction: return info def serialise_value(self, element, value): - return element.walk(lambda v: isinstance(v, entity_instance), lambda v: {"id": v.id()}, value) + return element.walk( + lambda v: isinstance(v, entity_instance), + lambda v: {"id": v.id()} if v.id() else {"type": v.is_a(), "value": v.wrappedValue}, + value, + ) def unserialise_value(self, element, value): - return element.walk(lambda v: isinstance(v, dict), lambda v: self.file.by_id(v["id"]), value) + return element.walk( + lambda v: isinstance(v, dict), + lambda v: self.file.by_id(v["id"]) if v.get("id") else self.file.create_entity(v["type"], v["value"]), + value, + ) def batch(self): self.is_batched = True