From 4e62c276ee64eefbbdd3f9393b36a51902ae1c13 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 2 Dec 2020 09:51:00 +1100 Subject: [PATCH] Fix #1111 - issue where sometimes class and product dropdown gets out of sync. Also new utility function to query is_a() in the schema. --- .../blenderbim/bim/operator.py | 58 +++++++++--- .../ifcopenshell/util/schema.py | 91 ++++++++++++------- 2 files changed, 103 insertions(+), 46 deletions(-) diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index d636de9d80..685fc91ef3 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -10,6 +10,7 @@ import ifcopenshell import ifcopenshell.util.selector import ifcopenshell.util.geolocation import ifcopenshell.util.pset +import ifcopenshell.util.schema import tempfile from . import export_ifc from . import import_ifc @@ -220,7 +221,22 @@ class ReassignClass(bpy.types.Operator): def execute(self, context): obj = bpy.context.active_object + ifc_class = obj.name.split("/")[0] + ifc_schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(bpy.context.scene.BIMProperties.export_schema) bpy.context.active_object.BIMObjectProperties.is_reassigning_class = True + ifc_products = [ + "IfcElement", + "IfcElementType", + "IfcSpatialElement", + "IfcGroup", + "IfcStructural", + "IfcPositioningElement", + "IfcContext", + "IfcAnnotation", + ] + for ifc_product in ifc_products: + if ifcopenshell.util.schema.is_a(ifc_schema.declaration_by_name(ifc_class), ifc_product): + bpy.context.scene.BIMProperties.ifc_product = ifc_product bpy.context.scene.BIMProperties.ifc_class = obj.name.split("/")[0] predefined_type = obj.BIMObjectProperties.attributes.get("PredefinedType") if predefined_type: @@ -1801,6 +1817,7 @@ class SelectClashSource(bpy.types.Operator): context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} + class SelectClashResults(bpy.types.Operator): bl_idname = "bim.select_clash_results" bl_label = "Select Clash Results" @@ -1814,6 +1831,7 @@ class SelectClashResults(bpy.types.Operator): context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} + class SelectSmartGroupedClashesPath(bpy.types.Operator): bl_idname = "bim.select_smart_grouped_clashes_path" bl_label = "Select Smart-Grouped Clashes Path" @@ -1827,6 +1845,7 @@ class SelectSmartGroupedClashesPath(bpy.types.Operator): context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} + class ExecuteIfcClash(bpy.types.Operator): bl_idname = "bim.execute_ifc_clash" bl_label = "Execute IFC Clash" @@ -1901,6 +1920,7 @@ class SelectIfcClashResults(bpy.types.Operator): obj.select_set(True) return {"FINISHED"} + class SmartClashGroup(bpy.types.Operator): bl_idname = "bim.smart_clash_group" bl_label = "Smart Group Clashes" @@ -1918,19 +1938,21 @@ class SmartClashGroup(bpy.types.Operator): with open(self.filepath) as f: clash_sets = json.load(f) - + # execute the smart grouping save_path = bpy.path.ensure_ext(bpy.context.scene.BIMProperties.smart_grouped_clashes_path, ".json") - smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, bpy.context.scene.BIMProperties.smart_clash_grouping_max_distance) + smart_grouped_clashes = ifc_clasher.smart_group_clashes( + clash_sets, bpy.context.scene.BIMProperties.smart_clash_grouping_max_distance + ) # save smart_groups to json - with open(save_path, 'w') as f: + with open(save_path, "w") as f: f.write(json.dumps(smart_grouped_clashes)) clash_set_name = bpy.context.scene.BIMProperties.clash_sets[ bpy.context.scene.BIMProperties.active_clash_set_index ].name - + # Reset the list of smart_clash_groups for the UI bpy.context.scene.BIMProperties.smart_clash_groups.clear() @@ -1954,7 +1976,7 @@ class SmartClashGroup(bpy.types.Operator): class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): bl_idname = "bim.load_smart_groups_for_active_clash_set" bl_label = "Load Smart Groups for Active Clash Set" - + def execute(self, context): smart_groups_path = bpy.path.ensure_ext(bpy.context.scene.BIMProperties.smart_grouped_clashes_path, ".json") @@ -1967,7 +1989,7 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): # Reset the list of smart_clash_groups for the UI bpy.context.scene.BIMProperties.smart_clash_groups.clear() - + for clash_set, smart_groups in smart_grouped_clashes.items(): # Only select the clashes that correspond to the actively selected IFC Clash Set if clash_set != clash_set_name: @@ -1990,21 +2012,24 @@ class SelectSmartGroup(bpy.types.Operator): def execute(self, context): # Select smart group in view - selected_smart_group = bpy.context.scene.BIMProperties.smart_clash_groups[bpy.context.scene.BIMProperties.active_smart_group_index] - #print(selected_smart_group.number) + selected_smart_group = bpy.context.scene.BIMProperties.smart_clash_groups[ + bpy.context.scene.BIMProperties.active_smart_group_index + ] + # print(selected_smart_group.number) for obj in bpy.context.visible_objects: global_id = obj.BIMObjectProperties.attributes.get("GlobalId") if global_id: for id in selected_smart_group.global_ids: - #print("Id: ", id) - #print("Global id: ", global_id.string_value) + # print("Id: ", id) + # print("Global id: ", global_id.string_value) if global_id.string_value in id.name: - #print("object match: ", global_id) + # print("object match: ", global_id) obj.select_set(True) return {"FINISHED"} - + + class SelectBcfFile(bpy.types.Operator): bl_idname = "bim.select_bcf_file" bl_label = "Select BCF File" @@ -2555,7 +2580,8 @@ class CutSection(bpy.types.Operator): def does_obj_have_target_view_representation(self, obj, camera): return camera.data.BIMCameraProperties.target_view in [ - c.target_view for c in obj.BIMObjectProperties.representation_contexts + c.target_view + for c in obj.BIMObjectProperties.representation_contexts if c.context == "Plan" and c.name == "Annotation" ] @@ -2713,7 +2739,11 @@ class SwitchContext(bpy.types.Operator): self.obj.data.name = "Model/Body/MODEL_VIEW/" + self.obj.data.name has_default_context = False for subcontext in self.obj.BIMObjectProperties.representation_contexts: - if subcontext.context == "Model" and subcontext.name == "Body" and subcontext.target_view == "MODEL_VIEW": + if ( + subcontext.context == "Model" + and subcontext.name == "Body" + and subcontext.target_view == "MODEL_VIEW" + ): has_default_context = True break if not has_default_context: diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index d408f0dc17..acffe3967f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -8,6 +8,16 @@ import ifcopenshell cwd = os.path.dirname(os.path.realpath(__file__)) + +def is_a(entity, ifc_class): + ifc_class = ifc_class.lower() + if entity.name_lc() == ifc_class: + return True + if entity.supertype(): + return is_a(entity.supertype(), ifc_class) + return False + + class Migrator: def __init__(self): self.migrated_ids = {} @@ -70,13 +80,13 @@ class Migrator: return new_file.by_id(self.migrated_ids[element.id()]) except: pass - #print("Migrating", element) + # print("Migrating", element) schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(new_file.schema) new_element = self.migrate_class(element, new_file) - #print("Migrated class from {} to {}".format(element, new_element)) + # print("Migrated class from {} to {}".format(element, new_element)) new_element_schema = schema.declaration_by_name(new_element.is_a()) if not hasattr(new_element_schema, "all_attributes"): - return element # The element has no attributes, so migration is done + return element # The element has no attributes, so migration is done new_element = self.migrate_attributes(element, new_file, new_element, new_element_schema) self.migrated_ids[element.id()] = new_element.id() return new_element @@ -102,39 +112,47 @@ class Migrator: return new_element def migrate_attribute(self, attribute, element, new_file, new_element, new_element_schema): - #print("Migrating attribute", element, new_element, attribute.name()) + # print("Migrating attribute", element, new_element, attribute.name()) if hasattr(element, attribute.name()): value = getattr(element, attribute.name()) - #print("Attribute names matched", value) + # print("Attribute names matched", value) elif new_file.schema == "IFC2X3": # IFC4 to IFC2X3: We know the IFC2X3 attribute name, but not its IFC4 equivalent - #print("Searching for an equivalent", new_element, attribute.name()) + # print("Searching for an equivalent", new_element, attribute.name()) try: equivalent_map = self.attribute_4_to_2x3[new_element.is_a()] equivalent = list(equivalent_map.keys())[list(equivalent_map.values()).index(attribute.name())] if hasattr(element, equivalent): - #print("Equivalent found", equivalent) + # print("Equivalent found", equivalent) value = getattr(element, equivalent) else: return except: - print("Unable to find equivalent attribute of {} to migrate from {} to {}".format(attribute.name(), element, new_element)) - return # We tried our best + print( + "Unable to find equivalent attribute of {} to migrate from {} to {}".format( + attribute.name(), element, new_element + ) + ) + return # We tried our best elif new_file.schema == "IFC4": # IFC2X3 to IFC4: We know the IFC4 attribute name, but not its IFC2X3 equivalent - #print("Searching for an equivalent", element, new_element, attribute.name()) + # print("Searching for an equivalent", element, new_element, attribute.name()) try: equivalent = self.attribute_4_to_2x3[new_element.is_a()][attribute.name()] - #print("Searching for equivalent", equivalent) + # print("Searching for equivalent", equivalent) if hasattr(element, equivalent): value = getattr(element, equivalent) else: return except: - print("Unable to find equivalent attribute of {} to migrate from {} to {}".format(attribute.name(), element, new_element)) - return # We tried our best + print( + "Unable to find equivalent attribute of {} to migrate from {} to {}".format( + attribute.name(), element, new_element + ) + ) + return # We tried our best - #print("Continuing migration of {} to migrate from {} to {}".format(attribute.name(), element, new_element)) + # print("Continuing migration of {} to migrate from {} to {}".format(attribute.name(), element, new_element)) if value is None and not attribute.optional(): value = self.generate_default_value(attribute, new_file) if value is None: @@ -155,24 +173,33 @@ class Migrator: elif self.default_entities[attribute.name()]: return self.default_entities[attribute.name()] elif attribute.name() == "OwnerHistory": - self.default_entities[attribute.name()] = new_file.create_entity("IfcOwnerHistory", **{ - "OwningUser": new_file.create_entity("IfcPersonAndOrganization", **{ - "ThePerson": new_file.create_entity("IfcPerson"), - "TheOrganization": new_file.create_entity("IfcOrganization", **{ - "Name": "IfcOpenShell Migrator" - }) - }), - "OwningApplication": new_file.create_entity("IfcApplication", **{ - "ApplicationDeveloper": new_file.create_entity("IfcOrganization", **{ - "Name": "IfcOpenShell Migrator" - }), - "Version": "Works for me", - "ApplicationFullName": "IfcOpenShell Migrator", - "ApplicationIdentifier": "IfcOpenShell Migrator", - }), - "ChangeAction": "NOCHANGE", - "CreationDate": int(time.time()) - }) + self.default_entities[attribute.name()] = new_file.create_entity( + "IfcOwnerHistory", + **{ + "OwningUser": new_file.create_entity( + "IfcPersonAndOrganization", + **{ + "ThePerson": new_file.create_entity("IfcPerson"), + "TheOrganization": new_file.create_entity( + "IfcOrganization", **{"Name": "IfcOpenShell Migrator"} + ), + } + ), + "OwningApplication": new_file.create_entity( + "IfcApplication", + **{ + "ApplicationDeveloper": new_file.create_entity( + "IfcOrganization", **{"Name": "IfcOpenShell Migrator"} + ), + "Version": "Works for me", + "ApplicationFullName": "IfcOpenShell Migrator", + "ApplicationIdentifier": "IfcOpenShell Migrator", + } + ), + "ChangeAction": "NOCHANGE", + "CreationDate": int(time.time()), + } + ) return self.default_entities[attribute.name()]