From 2596f90015e67b4e515318b8703be14c8ccdd2a5 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 30 Nov 2020 19:37:53 +1100 Subject: [PATCH] Fix #952. Implement roundtripping of all representation contexts. --- .../blenderbim/bim/export_ifc.py | 120 +++++++++++------- .../blenderbim/bim/import_ifc.py | 26 +--- .../blenderbim/bim/operator.py | 42 +++++- src/ifcblenderexport/blenderbim/bim/prop.py | 2 +- 4 files changed, 115 insertions(+), 75 deletions(-) diff --git a/src/ifcblenderexport/blenderbim/bim/export_ifc.py b/src/ifcblenderexport/blenderbim/bim/export_ifc.py index cdc37c5973..6ade13bbb6 100644 --- a/src/ifcblenderexport/blenderbim/bim/export_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/export_ifc.py @@ -1023,8 +1023,6 @@ class IfcParser: obj.data, obj, "Model", "Body", "MODEL_VIEW" ) if "Model/Box/MODEL_VIEW" in self.generated_subcontexts: - if self.ifc_export_settings.should_roundtrip_native and obj.data.BIMMeshProperties.ifc_definition_id: - return self.representations["Model/Box/MODEL_VIEW/{}".format(obj.data.name)] = self.get_representation( obj.data, obj, "Model", "Box", "MODEL_VIEW" ) @@ -1057,21 +1055,44 @@ class IfcParser: for context in self.ifc_export_settings.context_tree: for subcontext in context["subcontexts"]: for target_view in subcontext["target_views"]: - self.append_representation_in_context(obj, context["name"], subcontext["name"], target_view, name) + rep_context = self.get_obj_representation_context(obj, context["name"], subcontext["name"], target_view) + if rep_context: + self.append_representation_in_context(obj, rep_context, name) + + def get_obj_representation_context(self, obj, context, subcontext, target_view): + for c in obj.BIMObjectProperties.representation_contexts: + if c.context == context and c.name == subcontext and c.target_view == target_view: + return c + if obj.BIMObjectProperties.representation_contexts: + return + if context == "Model" and subcontext == "Body" and target_view == "MODEL_VIEW": + representation_context = obj.BIMObjectProperties.representation_contexts.add() + representation_context.context = "Model" + representation_context.name = "Body" + representation_context.target_view = "MODEL_VIEW" + return representation_context + + def append_representation_in_context(self, obj, rep_context, name): + context = rep_context.context + subcontext = rep_context.name + target_view = rep_context.target_view + + if self.ifc_export_settings.should_roundtrip_native and rep_context.ifc_definition_id: + self.representations[ + "{}/{}/{}/{}".format(context, subcontext, target_view, name) + ] = self.get_representation(obj.data, obj, context, subcontext, target_view) + return - def append_representation_in_context(self, obj, context, subcontext, target_view, name): context_prefix = "/".join([context, subcontext, target_view]) mesh_name = "/".join([context_prefix, name]) mesh = self.search_for_mesh_or_curve_data(mesh_name) + # TODO: if the search result is empty, we should check for ifc_definition_id if mesh: self.representations[mesh_name] = self.get_representation(mesh, obj, context, subcontext, target_view) if "Model/Box/MODEL_VIEW" in self.generated_subcontexts and context_prefix == "Model/Body/MODEL_VIEW": - if self.ifc_export_settings.should_roundtrip_native and obj.data.BIMMeshProperties.ifc_definition_id: - pass - else: - self.representations[ - "Model/Box/MODEL_VIEW/{}".format(mesh_name.split("/")[3]) - ] = self.get_representation(obj.data, obj, "Model", "Box", "MODEL_VIEW") + self.representations[ + "Model/Box/MODEL_VIEW/{}".format(mesh_name.split("/")[3]) + ] = self.get_representation(obj.data, obj, "Model", "Box", "MODEL_VIEW") elif ( context_prefix == "Model/Body/MODEL_VIEW" and obj.data and not self.is_mesh_context_sensitive(obj.data.name) ): @@ -1090,6 +1111,7 @@ class IfcParser: return data def get_representation(self, mesh, obj, context, subcontext, target_view): + rep_context = self.get_obj_representation_context(obj, context, subcontext, target_view) return { "ifc": None, "raw": mesh, @@ -1097,11 +1119,9 @@ class IfcParser: "context": context, "subcontext": subcontext, "target_view": target_view, - "has_ifc_definition": False - if not hasattr(mesh, "BIMMeshProperties") - else (mesh.BIMMeshProperties.ifc_definition or mesh.BIMMeshProperties.ifc_definition_id), + "has_ifc_definition": rep_context and rep_context.ifc_definition_id, "ifc_definition": mesh.BIMMeshProperties.ifc_definition if hasattr(mesh, "BIMMeshProperties") else None, - "ifc_definition_id": mesh.BIMMeshProperties.ifc_definition_id + "ifc_definition_id": rep_context.ifc_definition_id if rep_context else 0 if hasattr(mesh, "BIMMeshProperties") else None, "is_parametric": mesh.BIMMeshProperties.is_parametric if hasattr(mesh, "BIMMeshProperties") else False, @@ -1116,7 +1136,7 @@ class IfcParser: "presentation_layer": mesh.BIMMeshProperties.presentation_layer_index if hasattr(mesh, "BIMMeshProperties") and mesh.BIMMeshProperties.presentation_layer_index != -1 else False, - "attributes": {"Name": mesh.name}, + "attributes": {"Name": mesh.name if mesh else ""}, } def is_wireframe_mesh(self, mesh, obj): @@ -1323,7 +1343,8 @@ class IfcExporter: self.template_file = "{}template.ifc".format(ifc_export_settings.schema_dir) self.ifc_export_settings = ifc_export_settings self.ifc_parser = ifc_parser - self.roundtrip_id_map = {} + self.migrator = ifcopenshell.util.schema.Migrator() + self.roundtrip_id_new_to_old = {} def export(self, selected_objects): self.schema_version = self.ifc_export_settings.schema @@ -1633,6 +1654,7 @@ class IfcExporter: if self.file.schema == "IFC4": classification["ifc"] = self.file.add(classification["raw_element"]) else: + # TODO: Check if we can use self.migrator instead migrator = ifcopenshell.util.schema.Migrator() classification["ifc"] = migrator.migrate(classification["raw_element"], self.file) self.file.createIfcRelAssociatesClassification( @@ -1649,6 +1671,7 @@ class IfcExporter: if self.file.schema == "IFC4": reference["ifc"] = self.file.add(reference["raw_element"]) else: + # TODO: Check if we can use self.migrator instead migrator = ifcopenshell.util.schema.Migrator() reference["ifc"] = migrator.migrate(reference["raw_element"], self.file) @@ -2023,10 +2046,11 @@ class IfcExporter: # in edge cases. if representation.RepresentationIdentifier != "Body": continue - if product["raw"].data.BIMMeshProperties.ifc_definition_id: + rep_context = self.ifc_parser.get_obj_representation_context(product["raw"], "Model", "Body", "MODEL_VIEW") + if rep_context and rep_context.ifc_definition_id: # For native roundtripping, each slot could be a one to many relationship to items for item in self.get_geometric_representation_items(representation): - original_id = self.roundtrip_id_map[item.id()] + original_id = self.roundtrip_id_new_to_old[item.id()] i = product["raw"].data.BIMMeshProperties.ifc_item_ids.get(str(original_id)).slot_index material_slots.append((product["raw"].material_slots[i].name, item)) else: @@ -2384,38 +2408,40 @@ class IfcExporter: if representation["ifc_definition"]: print("Authoring an IFC definition directly is not yet implemented") return - elif representation["ifc_definition_id"]: + if representation["ifc_definition_id"]: + return self.create_representation_from_definition_id(representation) + + def create_representation_from_definition_id(self, representation): + if self.file.schema == ifc.IfcStore.get_file().schema: + entry = self.file.add(ifc.IfcStore.get_file().by_id(representation["ifc_definition_id"])) + else: + entry = self.migrator.migrate(ifc.IfcStore.get_file().by_id(representation["ifc_definition_id"]), self.file) + + substitutions = {"contexts": []} + + representation_elements = ifc.IfcStore.get_file().traverse( + ifc.IfcStore.get_file().by_id(representation["ifc_definition_id"]) + ) + + for element in representation_elements: if self.file.schema == ifc.IfcStore.get_file().schema: - entry = self.file.add(ifc.IfcStore.get_file().by_id(representation["ifc_definition_id"])) + added_element = self.file.add(element) else: - migrator = ifcopenshell.util.schema.Migrator() - entry = migrator.migrate(ifc.IfcStore.get_file().by_id(representation["ifc_definition_id"]), self.file) + added_element = self.migrator.migrate(element, self.file) + if added_element.is_a("IfcGeometricRepresentationContext"): + substitutions["contexts"].append(added_element) + elif added_element.is_a("IfcGeometricRepresentationItem"): + self.roundtrip_id_new_to_old[added_element.id()] = element.id() - substitutions = {"contexts": []} - - representation_elements = ifc.IfcStore.get_file().traverse( - ifc.IfcStore.get_file().by_id(representation["ifc_definition_id"]) - ) - - for element in representation_elements: - if self.file.schema == ifc.IfcStore.get_file().schema: - added_element = self.file.add(element) - else: - added_element = migrator.migrate(element, self.file) - if added_element.is_a("IfcGeometricRepresentationContext"): - substitutions["contexts"].append(added_element) - elif added_element.is_a("IfcGeometricRepresentationItem"): - self.roundtrip_id_map[added_element.id()] = element.id() - - for element in substitutions["contexts"]: - new_element = self.ifc_rep_context[representation["context"]][representation["subcontext"]][ - representation["target_view"] - ]["ifc"] - for inverse in self.file.get_inverse(element): - ifcopenshell.util.element.replace_attribute(inverse, element, new_element) - # TODO: Work out how and when to purge this - # self.file.remove(element) - return entry + for element in substitutions["contexts"]: + new_element = self.ifc_rep_context[representation["context"]][representation["subcontext"]][ + representation["target_view"] + ]["ifc"] + for inverse in self.file.get_inverse(element): + ifcopenshell.util.element.replace_attribute(inverse, element, new_element) + # TODO: Work out how and when to purge this + # self.file.remove(element) + return entry def create_model_representation(self, representation): if representation["subcontext"] == "Annotation": diff --git a/src/ifcblenderexport/blenderbim/bim/import_ifc.py b/src/ifcblenderexport/blenderbim/bim/import_ifc.py index adeae61358..7812e25596 100644 --- a/src/ifcblenderexport/blenderbim/bim/import_ifc.py +++ b/src/ifcblenderexport/blenderbim/bim/import_ifc.py @@ -1366,10 +1366,12 @@ class IfcImporter: def add_product_representation_contexts(self, element, obj): subcontexts = [] + ifc_definition_ids = [] if element.is_a("IfcProduct"): if not element.Representation: return for r in element.Representation.Representations: + ifc_definition_ids.append(r.id()) if r.ContextOfItems.is_a("IfcGeometricRepresentationSubContext"): subcontexts.append( "{}/{}/{}".format( @@ -1388,6 +1390,7 @@ class IfcImporter: if not element.RepresentationMaps: return for r in element.RepresentationMaps: + ifc_definition_ids.append(r.id()) if r.MappedRepresentation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext"): subcontexts.append( "{}/{}/{}".format( @@ -1404,14 +1407,14 @@ class IfcImporter: "", ) ) - subcontexts = set(subcontexts) - for subcontext in subcontexts: + for i, subcontext in enumerate(subcontexts): representation_context = obj.BIMObjectProperties.representation_contexts.add() ( representation_context.context, representation_context.name, representation_context.target_view, ) = subcontext.split("/") + representation_context.ifc_definition_id = ifc_definition_ids[i] def add_product_definitions(self, element, obj): if not hasattr(element, "IsDefinedBy") or not element.IsDefinedBy: @@ -2059,11 +2062,6 @@ class IfcImporter: results.append({"raw": representation, "matrix": self.scale_matrix(matrix)}) return results - def get_representation_of_context(self, representations, context): - for representation in representations: - if representation.RepresentationIdentifier == context: - return representation - def scale_matrix(self, matrix): matrix[0][3] *= self.unit_scale matrix[1][3] *= self.unit_scale @@ -2164,25 +2162,13 @@ class IfcImporter: ios_materials.append(mat.name) mesh["ios_materials"] = ios_materials mesh["ios_material_ids"] = geometry.material_ids - self.store_representation_source(mesh, element, shape) + mesh.BIMMeshProperties.geometry_type = str(self.get_geometry_type(element)) return mesh except: self.ifc_import_settings.logger.error("Could not create mesh for %s", element) import traceback - print(traceback.format_exc()) - def store_representation_source(self, mesh, element, shape): - # TODO Refactor to specialist class - mesh.BIMMeshProperties.geometry_type = str(self.get_geometry_type(element)) - if not self.ifc_import_settings.should_roundtrip_native: - return - if element.is_a("IfcRepresentation"): - representation = element - else: - representation = self.get_representation_of_context(element.Representation.Representations, shape.context) - mesh.BIMMeshProperties.ifc_definition_id = int(representation.id()) - def create_curve(self, geometry): curve = bpy.data.curves.new(geometry.id, type="CURVE") curve.dimensions = "3D" diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index 82a75f0bfe..5cb3b3b0e9 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -2579,10 +2579,16 @@ class SwitchContext(bpy.types.Operator): if "/" not in self.obj.data.name: self.obj.data.name = ifcopenshell.guid.compress(str(uuid.uuid4()).replace("-", "")) self.obj.data.name = "Model/Body/MODEL_VIEW/" + self.obj.data.name - representation_context = self.obj.BIMObjectProperties.representation_contexts.add() - representation_context.context = "Model" - representation_context.name = "Body" - representation_context.target_view = "MODEL_VIEW" + 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": + has_default_context = True + break + if not has_default_context: + representation_context = self.obj.BIMObjectProperties.representation_contexts.add() + representation_context.context = "Model" + representation_context.name = "Body" + representation_context.target_view = "MODEL_VIEW" self.context = bpy.context.scene.BIMProperties.available_contexts self.subcontext = bpy.context.scene.BIMProperties.available_subcontexts @@ -4512,8 +4518,12 @@ class GetRepresentationIfcParameters(bpy.types.Operator): bl_label = "Get Representation IFC Parameters" def execute(self, context): - props = bpy.context.active_object.data.BIMMeshProperties - elements = ifc.IfcStore.get_file().traverse(ifc.IfcStore.get_file().by_id(props.ifc_definition_id)) + obj = bpy.context.active_object + props = obj.data.BIMMeshProperties + element_id = self.get_ifc_definition_id(obj) + if not element_id: + return {"FINISHED"} + elements = ifc.IfcStore.get_file().traverse(ifc.IfcStore.get_file().by_id(element_id)) for element in elements: if not element.is_a("IfcRepresentationItem"): continue @@ -4528,6 +4538,15 @@ class GetRepresentationIfcParameters(bpy.types.Operator): new.value = element[i] return {"FINISHED"} + def get_ifc_definition_id(self, obj): + if "/" not in obj.data.name: + context, subcontext, target_view = ("Model", "Body", "MODEL_VIEW") + else: + context, subcontext, target_view = obj.data.name.split("/")[0:3] + for c in obj.BIMObjectProperties.representation_contexts: + if c.context == context and c.name == subcontext and c.target_view == target_view: + return c.ifc_definition_id or None + class UpdateIfcRepresentation(bpy.types.Operator): bl_idname = "bim.update_ifc_representation" @@ -4545,7 +4564,7 @@ class UpdateIfcRepresentation(bpy.types.Operator): props = bpy.context.active_object.data.BIMMeshProperties logger = logging.getLogger("ImportIFC") self.ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, ifc.IfcStore.path, logger) - element = ifc.IfcStore.get_file().by_id(props.ifc_definition_id) + element = ifc.IfcStore.get_file().by_id(self.get_ifc_definition_id(bpy.context.active_object)) settings = ifcopenshell.geom.settings() shape = ifcopenshell.geom.create_shape(settings, element) ifc_importer = import_ifc.IfcImporter(self.ifc_import_settings) @@ -4556,6 +4575,15 @@ class UpdateIfcRepresentation(bpy.types.Operator): if ifc_importer.material_creator.parse_representation(element): ifc_importer.material_creator.assign_material_slots_to_faces(bpy.context.active_object) + def get_ifc_definition_id(self, obj): + if "/" not in obj.data.name: + context, subcontext, target_view = ("Model", "Body", "MODEL_VIEW") + else: + context, subcontext, target_view = obj.data.name.split("/")[0:3] + for c in obj.BIMObjectProperties.representation_contexts: + if c.context == context and c.name == subcontext and c.target_view == target_view: + return c.ifc_definition_id or None + class BlenderClasher: def process_clash_set(self): diff --git a/src/ifcblenderexport/blenderbim/bim/prop.py b/src/ifcblenderexport/blenderbim/bim/prop.py index 780af9cb4e..8879ed87b6 100644 --- a/src/ifcblenderexport/blenderbim/bim/prop.py +++ b/src/ifcblenderexport/blenderbim/bim/prop.py @@ -553,6 +553,7 @@ class Subcontext(PropertyGroup): name: StringProperty(name="Name") context: StringProperty(name="Context") target_view: StringProperty(name="Target View") + ifc_definition_id: IntProperty(name="IFC Definition ID") class MaterialLayer(PropertyGroup): @@ -1732,7 +1733,6 @@ class BIMMeshProperties(PropertyGroup): is_parametric: BoolProperty(name="Is Parametric", default=False) geometry_type: StringProperty(name="Geometry Type") ifc_definition: StringProperty(name="IFC Definition") - ifc_definition_id: IntProperty(name="IFC Definition ID") ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter) active_representation_item_index: IntProperty(name="Active Representation Item Index") presentation_layer_index: IntProperty(name="Presentation Layer Index", default=-1)