From 30b30903860b8b40bf0fccadaf2508299233249d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 5 Jan 2021 13:16:31 +1100 Subject: [PATCH] WIP migrate attribute management to partial editing with 4 new features! See #1222. New features include: - New clarity between null and empty string attributes - Attribute editing now shows appropriate UI widgets depending on data type - Attribute editing for enumerations now show a dropdown - Attribute editing is now schema sensitive --- .../blenderbim/bim/__init__.py | 6 +- .../blenderbim/bim/import_ifc.py | 4 - .../bim/module/attribute/__init__.py | 17 ++++ .../blenderbim/bim/module/attribute/data.py | 58 ++++++++++++ .../bim/module/attribute/edit_attributes.py | 13 +++ .../bim/module/attribute/operator.py | 88 ++++++++++++++++++ .../blenderbim/bim/module/attribute/ui.py | 92 +++++++++++++++++++ .../blenderbim/bim/module/root/data.py | 2 +- .../blenderbim/bim/module/spatial/data.py | 4 +- .../blenderbim/bim/module/spatial/ui.py | 12 ++- .../blenderbim/bim/operator.py | 38 -------- src/ifcblenderexport/blenderbim/bim/prop.py | 23 ++--- src/ifcblenderexport/blenderbim/bim/ui.py | 49 ---------- 13 files changed, 292 insertions(+), 114 deletions(-) create mode 100644 src/ifcblenderexport/blenderbim/bim/module/attribute/__init__.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/attribute/data.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/attribute/edit_attributes.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/attribute/operator.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/attribute/ui.py diff --git a/src/ifcblenderexport/blenderbim/bim/__init__.py b/src/ifcblenderexport/blenderbim/bim/__init__.py index efe716e7f0..8dfe823fee 100644 --- a/src/ifcblenderexport/blenderbim/bim/__init__.py +++ b/src/ifcblenderexport/blenderbim/bim/__init__.py @@ -6,6 +6,7 @@ bpy = sys.modules.get("bpy") if bpy is not None: import bpy import blenderbim.bim.module.root as module_root + import blenderbim.bim.module.attribute as module_attribute import blenderbim.bim.module.bcf as module_bcf import blenderbim.bim.module.context as module_context import blenderbim.bim.module.covetool as module_covetool @@ -87,8 +88,6 @@ if bpy is not None: operator.UnassignDocumentReference, operator.RemoveObjectDocumentReference, operator.GenerateGlobalId, - operator.AddAttribute, - operator.RemoveAttribute, operator.AddMaterialAttribute, operator.RemoveMaterialAttribute, operator.QuickProjectSetup, @@ -312,6 +311,7 @@ if bpy is not None: ] classes.extend(module_root.classes) + classes.extend(module_attribute.classes) classes.extend(module_bcf.classes) classes.extend(module_context.classes) classes.extend(module_covetool.classes) @@ -350,6 +350,7 @@ if bpy is not None: bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties) bpy.types.SCENE_PT_unit.append(ui.ifc_units) module_root.register() + module_attribute.register() module_bcf.register() module_context.register() module_covetool.register() @@ -382,5 +383,6 @@ if bpy is not None: module_covetool.unregister() module_context.unregister() module_bcf.unregister() + module_attribute.register() module_root.unregister() bpy.app.handlers.depsgraph_update_pre.remove(operator.depsgraph_update_pre_handler) diff --git a/src/ifcblenderexport/blenderbim/bim/import_ifc.py b/src/ifcblenderexport/blenderbim/bim/import_ifc.py index 71f15756c1..9738bf02cc 100644 --- a/src/ifcblenderexport/blenderbim/bim/import_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/import_ifc.py @@ -847,7 +847,6 @@ class IfcImporter: obj = bpy.data.objects.new(self.get_name(element), mesh) obj.BIMObjectProperties.ifc_definition_id = element.id() self.material_creator.create(element, obj, mesh) - self.add_element_attributes(element, obj.BIMObjectProperties) self.add_element_classifications(element, obj) self.add_element_document_relations(element, obj) self.add_type_product_psets(element, obj) @@ -975,7 +974,6 @@ class IfcImporter: obj.matrix_world = self.get_element_matrix(element) self.add_element_representation_items(element, obj) - self.add_element_attributes(element, obj.BIMObjectProperties) self.add_element_classifications(element, obj) self.add_element_document_relations(element, obj) self.add_defines_by_type_relation(element, obj) @@ -1732,7 +1730,6 @@ class IfcImporter: obj.instance_type = "COLLECTION" obj.instance_collection = collection self.place_object_in_spatial_tree(element, obj) - self.add_element_attributes(element, obj.BIMObjectProperties) self.add_element_classifications(element, obj) self.add_element_document_relations(element, obj) self.add_defines_by_type_relation(element, obj) @@ -1795,7 +1792,6 @@ class IfcImporter: obj.BIMObjectProperties.ifc_definition_id = element.id() self.material_creator.create(element, obj, mesh) obj.matrix_world = self.get_element_matrix(element, mesh_name) - self.add_element_attributes(element, obj.BIMObjectProperties) self.add_element_classifications(element, obj) self.add_element_document_relations(element, obj) self.add_defines_by_type_relation(element, obj) diff --git a/src/ifcblenderexport/blenderbim/bim/module/attribute/__init__.py b/src/ifcblenderexport/blenderbim/bim/module/attribute/__init__.py new file mode 100644 index 0000000000..9d28fb060d --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/attribute/__init__.py @@ -0,0 +1,17 @@ +import bpy +from . import ui, operator + +classes = ( + operator.EnableEditingAttributes, + operator.DisableEditingAttributes, + operator.EditAttributes, + ui.BIM_PT_attributes, +) + + +def register(): + pass + + +def unregister(): + pass diff --git a/src/ifcblenderexport/blenderbim/bim/module/attribute/data.py b/src/ifcblenderexport/blenderbim/bim/module/attribute/data.py new file mode 100644 index 0000000000..ab5d954d06 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/attribute/data.py @@ -0,0 +1,58 @@ +import ifcopenshell +from blenderbim.bim.ifc import IfcStore + + +class Data: + products = {} + + @classmethod + def load(cls, product_id): + file = IfcStore.get_file() + if not file: + return + product = file.by_id(product_id) + cls.products[product_id] = [] + attributes = product.get_info() + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema) + declaration = schema.declaration_by_name(product.is_a()) + for attribute in declaration.all_attributes(): + data_type = str(attribute.type_of_attribute()) + value = getattr(product, attribute.name()) + list_type = None + enum_items = () + if "" in data_type: + list_type = "string" + elif "" in data_type: + list_type = "float" + elif "" in data_type: + list_type = "integer" + data_type = "list" + value = None if value is None else str(value) + elif "" in data_type: + data_type = "string" + value = None if value is None else str(value) + elif "" in data_type: + data_type = "float" + value = None if value is None else float(value) + elif "" in data_type: + data_type = "integer" + value = None if value is None else int(value) + elif " 0: + props.attributes.remove(0) + for attribute in Data.products[props.ifc_definition_id]: + new = props.attributes.add() + if attribute["type"] == "entity": + continue + new.name = attribute["name"] + new.is_null = attribute["is_null"] + if attribute["type"] == "string" or attribute["type"] == "list": + new.string_value = attribute["value"] or "" + elif attribute["type"] == "integer": + new.int_value = attribute["value"] or 0 + elif attribute["type"] == "float": + new.float_value = attribute["value"] or 0. + elif attribute["type"] == "enum": + new.enum_items = json.dumps(attribute["enum_items"]) + if attribute["value"]: + new.enum_value = attribute["value"] + props.is_editing_attributes = True + return {"FINISHED"} + + +class DisableEditingAttributes(bpy.types.Operator): + bl_idname = "bim.disable_editing_attributes" + bl_label = "Disable Editing Attributes" + + def execute(self, context): + obj = bpy.context.active_object + props = obj.BIMObjectProperties + props.is_editing_attributes = False + return {"FINISHED"} + + +class EditAttributes(bpy.types.Operator): + bl_idname = "bim.edit_attributes" + bl_label = "Edit Attributes" + + def execute(self, context): + self.file = IfcStore.get_file() + obj = bpy.context.active_object + props = obj.BIMObjectProperties + attributes = {} + for attribute in Data.products[props.ifc_definition_id]: + blender_attribute = props.attributes.get(attribute["name"]) + if not blender_attribute: + continue + if blender_attribute.is_null: + attributes[attribute["name"]] = None + elif attribute["type"] == "string": + attributes[attribute["name"]] = blender_attribute.string_value + elif attribute["type"] == "list": + values = blender_attribute.string_value[1:-1].split(", ") + print(attribute["name"]) + print(attribute["list_type"]) + if attribute["list_type"] == "float": + values = [float(v) for v in values] + elif attribute["list_type"] == "integer": + values = [int(v) for v in values] + attributes[attribute["name"]] = values + elif attribute["type"] == "integer": + attributes[attribute["name"]] = blender_attribute.int_value + elif attribute["type"] == "float": + attributes[attribute["name"]] = blender_attribute.float_value + elif attribute["type"] == "enum": + attributes[attribute["name"]] = blender_attribute.enum_value + usecase = edit_attributes.Usecase(self.file, { + "product": self.file.by_id(props.ifc_definition_id), + "attributes": attributes + }) + usecase.execute() + Data.load(props.ifc_definition_id) + bpy.ops.bim.disable_editing_attributes() + return {"FINISHED"} diff --git a/src/ifcblenderexport/blenderbim/bim/module/attribute/ui.py b/src/ifcblenderexport/blenderbim/bim/module/attribute/ui.py new file mode 100644 index 0000000000..1454159966 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/attribute/ui.py @@ -0,0 +1,92 @@ +from bpy.types import Panel +from blenderbim.bim.module.attribute.data import Data + + +class BIM_PT_attributes(Panel): + bl_label = "IFC Attributes" + bl_idname = "BIM_PT_attributes" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + @classmethod + def poll(cls, context): + return bool(context.active_object.BIMObjectProperties.ifc_definition_id) + + def draw(self, context): + props = context.active_object.BIMObjectProperties + if not props.ifc_definition_id: + return + if props.ifc_definition_id not in Data.products: + Data.load(props.ifc_definition_id) + + props = context.active_object.BIMObjectProperties + + + if props.is_editing_attributes: + row = self.layout.row(align=True) + row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes") + row.operator("bim.disable_editing_attributes", icon="X", text="") + + for attribute in Data.products[props.ifc_definition_id]: + if attribute["type"] == "entity": + continue + row = self.layout.row(align=True) + blender_attribute = props.attributes.get(attribute["name"]) + if attribute["type"] == "string" or attribute["type"] == "list": + row.prop(blender_attribute, "string_value", text=attribute["name"]) + elif attribute["type"] == "integer": + row.prop(blender_attribute, "int_value", text=attribute["name"]) + elif attribute["type"] == "float": + row.prop(blender_attribute, "float_value", text=attribute["name"]) + elif attribute["type"] == "enum": + row.prop(blender_attribute, "enum_value", text=attribute["name"]) + + if attribute["name"] == "GlobalId": + row.operator("bim.generate_global_id", icon="FILE_REFRESH", text="") + if attribute["is_optional"]: + row.prop(blender_attribute, "is_null", 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 = self.layout.row() + row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit") + for attribute in Data.products[props.ifc_definition_id]: + if attribute["value"] is None or attribute["type"] == "entity": + continue + row = self.layout.row(align=True) + row.label(text=attribute["name"]) + row.label(text=str(attribute["value"])) + + # TODO: reimplement, see #1222 + #if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name: + # self.draw_addresses_ui() + + def draw_addresses_ui(self): + self.layout.label(text="Address:") + address = bpy.context.active_object.BIMObjectProperties.address + row = self.layout.row() + row.prop(address, "purpose") + if address.purpose == "USERDEFINED": + row = self.layout.row() + row.prop(address, "user_defined_purpose") + row = self.layout.row() + row.prop(address, "description") + + row = self.layout.row() + row.prop(address, "internal_location") + row = self.layout.row() + row.prop(address, "address_lines") + row = self.layout.row() + row.prop(address, "postal_box") + row = self.layout.row() + row.prop(address, "town") + row = self.layout.row() + row.prop(address, "region") + row = self.layout.row() + row.prop(address, "postal_code") + row = self.layout.row() + row.prop(address, "country") + diff --git a/src/ifcblenderexport/blenderbim/bim/module/root/data.py b/src/ifcblenderexport/blenderbim/bim/module/root/data.py index 1e0449b93d..e0f186e1ac 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/root/data.py +++ b/src/ifcblenderexport/blenderbim/bim/module/root/data.py @@ -13,5 +13,5 @@ class Data: cls.products[product_id] = { "type": product.is_a(), "PredefinedType": product.PredefinedType if hasattr(product, "PredefinedType") else None, - "ObjectType": product.ObjectType or None + "ObjectType": product.ObjectType if hasattr(product, "ObjectType") else None } diff --git a/src/ifcblenderexport/blenderbim/bim/module/spatial/data.py b/src/ifcblenderexport/blenderbim/bim/module/spatial/data.py index b460defc52..cf74825e33 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/spatial/data.py +++ b/src/ifcblenderexport/blenderbim/bim/module/spatial/data.py @@ -10,7 +10,9 @@ class Data: if not file: return product = file.by_id(product_id) - if product.ContainedInStructure: + if not hasattr(product, "ContainedInStructure"): + cls.products[product_id] = None + elif product.ContainedInStructure: structure = product.ContainedInStructure[0].RelatingStructure cls.products[product_id] = {"type": structure.is_a(), "Name": structure.Name, "id": int(structure.id())} else: diff --git a/src/ifcblenderexport/blenderbim/bim/module/spatial/ui.py b/src/ifcblenderexport/blenderbim/bim/module/spatial/ui.py index e958ef168c..8780fe47de 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/spatial/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/module/spatial/ui.py @@ -11,7 +11,14 @@ class BIM_PT_spatial(Panel): @classmethod def poll(cls, context): - return bool(context.active_object.BIMObjectProperties.ifc_definition_id) + props = context.active_object.BIMObjectProperties + if not props.ifc_definition_id: + return False + if props.ifc_definition_id not in Data.products: + Data.load(props.ifc_definition_id) + if not Data.products[props.ifc_definition_id]: + return False + return True def draw(self, context): props = context.active_object.BIMObjectProperties @@ -20,9 +27,6 @@ class BIM_PT_spatial(Panel): if props.ifc_definition_id not in Data.products: Data.load(props.ifc_definition_id) - if "Ifc" not in context.active_object.name: - return - row = self.layout.row(align=True) name = "{}/{}".format( Data.products[props.ifc_definition_id]["type"], Data.products[props.ifc_definition_id]["Name"] diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index 2e6400b503..621e094eb6 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -910,28 +910,6 @@ class GenerateGlobalId(bpy.types.Operator): return {"FINISHED"} -class AddAttribute(bpy.types.Operator): - bl_idname = "bim.add_attribute" - bl_label = "Add Attribute" - - def execute(self, context): - if not bpy.context.active_object.BIMObjectProperties.applicable_attributes: - return {"FINISHED"} - name = bpy.context.active_object.BIMObjectProperties.applicable_attributes - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(bpy.context.scene.BIMProperties.export_schema) - for obj in bpy.context.selected_objects: - if "/" not in obj.name or obj.BIMObjectProperties.attributes.find(name) != -1: - continue - entity = schema.declaration_by_name(obj.name.split("/")[0]) - if name not in [a.name() for a in entity.all_attributes()]: - continue - attribute = obj.BIMObjectProperties.attributes.add() - attribute.name = name - if attribute.name == "GlobalId": - attribute.string_value = ifcopenshell.guid.new() - return {"FINISHED"} - - class AddMaterialAttribute(bpy.types.Operator): bl_idname = "bim.add_material_attribute" bl_label = "Add Material Attribute" @@ -943,22 +921,6 @@ class AddMaterialAttribute(bpy.types.Operator): return {"FINISHED"} -class RemoveAttribute(bpy.types.Operator): - bl_idname = "bim.remove_attribute" - bl_label = "Remove Attribute" - attribute_index: bpy.props.IntProperty() - - def execute(self, context): - name = bpy.context.active_object.BIMObjectProperties.attributes[self.attribute_index].name - for obj in bpy.context.selected_objects: - if "/" not in obj.name: - continue - index = obj.BIMObjectProperties.attributes.find(name) - if index != -1: - obj.BIMObjectProperties.attributes.remove(index) - return {"FINISHED"} - - class RemoveMaterialAttribute(bpy.types.Operator): bl_idname = "bim.remove_material_attribute" bl_label = "Remove Material Attribute" diff --git a/src/ifcblenderexport/blenderbim/bim/prop.py b/src/ifcblenderexport/blenderbim/bim/prop.py index 9fa99cf4a5..5c791203c4 100644 --- a/src/ifcblenderexport/blenderbim/bim/prop.py +++ b/src/ifcblenderexport/blenderbim/bim/prop.py @@ -274,6 +274,10 @@ def getIfcClasses(self, context): return classes_enum +def getAttributeEnumValues(self, context): + return [(e, e, "") for e in json.loads(self.enum_items)] + + def getProfileDef(self, context): global profiledef_enum if len(profiledef_enum) < 1: @@ -440,20 +444,6 @@ def getQtoNames(self, context): return qtonames_enum -def getApplicableAttributes(self, context): - global attributes_enum - attributes_enum.clear() - if "/" in context.active_object.name and context.active_object.name.split("/")[0] in schema.ifc.elements: - attributes_enum.extend( - [ - (a["name"], a["name"], "") - for a in schema.ifc.elements[context.active_object.name.split("/")[0]]["attributes"] - if self.attributes.find(a["name"]) == -1 - ] - ) - return attributes_enum - - def getApplicableMaterialAttributes(self, context): global materialattributes_enum materialattributes_enum.clear() @@ -548,6 +538,9 @@ class Attribute(PropertyGroup): bool_value: BoolProperty(name="Value") int_value: IntProperty(name="Value") float_value: FloatProperty(name="Value") + is_null: BoolProperty(name="Is Null") + enum_items: StringProperty(name="Value") + enum_value: EnumProperty(items=getAttributeEnumValues, name="Value") class MaterialLayer(PropertyGroup): @@ -1467,11 +1460,11 @@ class BIMObjectProperties(PropertyGroup): is_reassigning_class: BoolProperty(name="Is Reassigning Class") global_ids: CollectionProperty(name="GlobalIds", type=GlobalId) attributes: CollectionProperty(name="Attributes", type=Attribute) + is_editing_attributes: BoolProperty(name="Is Editing Attributes") relating_type: PointerProperty(name="Type Product", type=bpy.types.Object) relating_structure: PointerProperty(name="Spatial Container", type=bpy.types.Object) psets: CollectionProperty(name="Psets", type=PsetQto) qtos: CollectionProperty(name="Qtos", type=PsetQto) - applicable_attributes: EnumProperty(items=getApplicableAttributes, name="Attribute Names") document_references: CollectionProperty(name="Document References", type=DocumentReference) active_document_reference_index: IntProperty(name="Active Document Reference Index") constraints: CollectionProperty(name="Constraints", type=Constraint) diff --git a/src/ifcblenderexport/blenderbim/bim/ui.py b/src/ifcblenderexport/blenderbim/bim/ui.py index e7b73b0602..5ab2c9cc0c 100644 --- a/src/ifcblenderexport/blenderbim/bim/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/ui.py @@ -26,59 +26,10 @@ class BIM_PT_object(Panel): if "Ifc" not in context.active_object.name: return - layout.label(text="Attributes:") - row = layout.row(align=True) - row.prop(props, "applicable_attributes", text="") - row.operator("bim.add_attribute") - - for index, attribute in enumerate(props.attributes): - row = layout.row(align=True) - row.prop(attribute, "name", text="") - row.prop(attribute, "string_value", text="") - if attribute.name == "GlobalId": - row.operator("bim.generate_global_id", icon="FILE_REFRESH", text="") - op = row.operator("bim.copy_attribute_to_selection", icon="COPYDOWN", text="") - op.attribute_name = attribute.name - op.attribute_value = attribute.string_value - row.operator("bim.remove_attribute", icon="X", text="").attribute_index = index - - row = layout.row() - row.prop(props, "attributes") - - if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name: - self.draw_addresses_ui() - row = layout.row(align=True) row.prop(props, "relating_type") row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="") - def draw_addresses_ui(self): - layout = self.layout - layout.label(text="Address:") - address = bpy.context.active_object.BIMObjectProperties.address - row = layout.row() - row.prop(address, "purpose") - if address.purpose == "USERDEFINED": - row = layout.row() - row.prop(address, "user_defined_purpose") - row = layout.row() - row.prop(address, "description") - - row = layout.row() - row.prop(address, "internal_location") - row = layout.row() - row.prop(address, "address_lines") - row = layout.row() - row.prop(address, "postal_box") - row = layout.row() - row.prop(address, "town") - row = layout.row() - row.prop(address, "region") - row = layout.row() - row.prop(address, "postal_code") - row = layout.row() - row.prop(address, "country") - class BIM_PT_object_material(Panel): bl_label = "IFC Object Material"