From bea837299e0b369eb66c5eb1a991f78f2e04180f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 30 Aug 2024 16:25:34 +0500 Subject: [PATCH] root.reassign_class to handle switching occurrence class <-> type class #5260 Kind of experimental. The idea is that it will reassign class e.g. from IfcWindow to IfcWindowType (or vice versa) and will keep the IFC representations and property sets. Example - https://imgur.com/a/X7MHR0s --- src/bonsai/bonsai/bim/module/root/operator.py | 69 +++++---- src/bonsai/bonsai/bim/module/root/ui.py | 3 +- src/bonsai/test/tool/test_root.py | 2 +- .../ifcopenshell/api/root/reassign_class.py | 146 +++++++++++++++--- .../test/api/root/test_reassign_class.py | 103 +++++++++++- 5 files changed, 266 insertions(+), 57 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index ca84543dea..62586e8c30 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -77,6 +77,7 @@ class DisableReassignClass(bpy.types.Operator): class ReassignClass(bpy.types.Operator): bl_idname = "bim.reassign_class" bl_label = "Reassign IFC Class" + bl_description = "Reassign IFC class for selected objects" bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() @@ -89,71 +90,71 @@ class ReassignClass(bpy.types.Operator): else: objects = set(context.selected_objects + [context.active_object]) self.file = IfcStore.get_file() - predefined_type = context.scene.BIMRootProperties.ifc_predefined_type + root_props = context.scene.BIMRootProperties + ifc_product: str = root_props.ifc_product + ifc_class: str = root_props.ifc_class + type_ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(ifc_class)), None) + + predefined_type = root_props.ifc_predefined_type if predefined_type == "USERDEFINED": - predefined_type = context.scene.BIMRootProperties.ifc_userdefined_type + predefined_type = root_props.ifc_userdefined_type # NOTE: root.reassign_class # automatically will reassign class for other occurrences of the type # so we need to run it only for the types or non-typed elements - elements_to_reassign = set() + elements_to_reassign: dict[ifcopenshell.entity_instance, str] = dict() # need to update blender object name # for all elements that were changed in the process - elements_to_update = set() + elements_to_update: set[ifcopenshell.entity_instance] = set() for obj in objects: - obj.BIMObjectProperties.is_reassigning_class = False element = tool.Ifc.get_entity(obj) - if element.is_a("IfcTypeObject"): - elements_to_reassign.add(element) - elements_to_update.update(ifcopenshell.util.element.get_types(element)) + if not element: continue - # check if element is typed - element_type = ifcopenshell.util.element.get_type(element) - if element_type: - elements_to_reassign.add(element_type) + same_ifc_product = element.is_a(ifc_product) + + if not same_ifc_product: + if not (element.is_a("IfcElement") and ifc_product == "IfcElementType") and not ( + element.is_a("IfcElementType") and ifc_product == "IfcElement" + ): + self.report( + {"ERROR"}, f"Not supported class reassignment for object '{obj.name}' -> {ifc_product}." + ) + return {"CANCELLED"} + + obj.BIMObjectProperties.is_reassigning_class = False + if element.is_a("IfcTypeObject"): + elements_to_reassign[element] = ifc_class + elements_to_update.update(ifcopenshell.util.element.get_types(element)) + continue + elif same_ifc_product and (element_type := ifcopenshell.util.element.get_type(element)): + assert type_ifc_class + elements_to_reassign[element_type] = type_ifc_class elements_to_update.update(ifcopenshell.util.element.get_types(element_type)) continue # non-typed element - elements_to_reassign.add(element) + elements_to_reassign[element] = ifc_class # store elements to objects to update later as elements will get invalid # after class reassignment - elements_to_update = elements_to_update | elements_to_reassign + elements_to_update = elements_to_update | set(elements_to_reassign) objects_to_update = set(o for e in elements_to_update if (o := tool.Ifc.get_object(e))) - base_class = context.scene.BIMRootProperties.ifc_class - if context.scene.BIMRootProperties.ifc_product == "IfcElementType": - type_class = base_class - occurrence_classes = ifcopenshell.util.type.get_applicable_entities(type_class) - occurrence_class = None if len(occurrence_classes) == 0 else occurrence_classes[0] - else: - occurrence_class = base_class - type_classes = ifcopenshell.util.type.get_applicable_types(occurrence_class) - type_class = None if len(type_classes) == 0 else type_classes[0] - reassigned_elements = set() - for element in elements_to_reassign: - ifc_class = type_class if element.is_a("IfcTypeObject") else occurrence_class - if ifc_class is None: - self.report( - {"ERROR"}, - f"Couldn't find valid class for reassigning element of class {element.is_a()} based on class {base_class}", - ) - return {"CANCELLED"} - + for element, ifc_class_ in elements_to_reassign.items(): element = ifcopenshell.api.run( "root.reassign_class", self.file, product=element, - ifc_class=ifc_class, + ifc_class=ifc_class_, predefined_type=predefined_type, ) reassigned_elements.add(element) for obj in objects_to_update: obj.name = tool.Loader.get_name(tool.Ifc.get_entity(obj)) + tool.Collector.assign(obj) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py index 995d16cdcc..c8cec94d57 100644 --- a/src/bonsai/bonsai/bim/module/root/ui.py +++ b/src/bonsai/bonsai/bim/module/root/ui.py @@ -83,8 +83,7 @@ class BIM_PT_class(Panel): def draw_class_dropdowns(self, context, ifc_predefined_types, is_reassigning_class=False): props = context.scene.BIMRootProperties layout = self.layout - if not is_reassigning_class: - prop_with_search(layout, props, "ifc_product") + prop_with_search(layout, props, "ifc_product") prop_with_search(layout, props, "ifc_class") if ifc_predefined_types: prop_with_search(layout, props, "ifc_predefined_type") diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py index 235ed3b04e..dda03e1ed3 100644 --- a/src/bonsai/test/tool/test_root.py +++ b/src/bonsai/test/tool/test_root.py @@ -173,7 +173,7 @@ class TestSetObjectName(NewFile): class TestReassignClass(NewFile): - def test_run(self): + def test_reassigning_multiple_occurrences_of_the_same_type(self): bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc" bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py index 3326f0b123..4eca742939 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py @@ -17,10 +17,16 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api.aggregate +import ifcopenshell.api.geometry +import ifcopenshell.api.spatial +import ifcopenshell.api.type +import ifcopenshell.guid +import ifcopenshell.util.representation import ifcopenshell.util.type import ifcopenshell.util.schema import ifcopenshell.util.element -from typing import Optional +from typing import Optional, Union, Literal def reassign_class( @@ -46,15 +52,14 @@ def reassign_class( cannot be unambiguously derived, so you are required to manually check this. + Reassigning type class to occurrence (and vice versa) is supported. + :param product: The IfcProduct that you want to change the class of. :type product: ifcopenshell.entity_instance :param ifc_class: The new IFC class you want to change it to. - :type ifc_class: str,optional :param predefined_type: In case you want to change the predefined type too. User defined types are also allowed, just type what you want. - :type predefined_type: str,optional :return: The newly modified product. - :rtype: ifcopenshell.entity_instance Example: @@ -80,38 +85,141 @@ def reassign_class( class Usecase: - def execute(self): - element = self.reassign_class(self.settings["product"], self.settings["ifc_class"]) + file: ifcopenshell.file + def execute(self): + ifc_class: str = self.settings["ifc_class"] + product: ifcopenshell.entity_instance = self.settings["product"] + predefined_type: Union[str, None] = self.settings["predefined_type"] + + was_type_product_before = product.is_a("IfcTypeProduct") + schema = ifcopenshell.schema_by_name(self.file.schema) + is_type_product_after = schema.declaration_by_name(ifc_class)._is("IfcTypeProduct") + + if was_type_product_before == is_type_product_after: + return self.simple_reassignment(product, ifc_class, predefined_type) + + switch_type = "occurrence_to_type" if is_type_product_after else "type_to_occurrence" + + return self.switch_between_class_types( + product, + switch_type, + ifc_class, + predefined_type, + ) + + def switch_between_class_types( + self, + element: ifcopenshell.entity_instance, + switch_type: Literal["occurrence_to_type", "type_to_occurrence"], + ifc_class: str, + predefined_type: Union[str, None], + ) -> ifcopenshell.entity_instance: + + psets_to_reassign: list[ifcopenshell.entity_instance] = [] + + representations = list(ifcopenshell.util.representation.get_representations_iter(element)) + for rep in representations: + if switch_type == "type_to_occurrence": + rep = ifcopenshell.util.representation.resolve_representation(rep) + ifcopenshell.api.geometry.unassign_representation(self.file, product=element, representation=rep) + + if switch_type == "type_to_occurrence": + occurrences = ifcopenshell.util.element.get_types(element) + psets_to_reassign = element.HasPropertySets or [] + + element = self.reassign_class(element, ifc_class, predefined_type) + ifcopenshell.api.type.unassign_type(self.file, occurrences) + + for pset in psets_to_reassign: + rel = self.file.create_entity( + "IfcRelDefinesByProperties", + GlobalId=ifcopenshell.guid.new(), + RelatedObjects=[element], + RelatingPropertyDefinition=pset, + ) + else: # occurrence_to_type + element_type = ifcopenshell.util.element.get_type(element) + # Handle element type. + if element_type: + ifcopenshell.api.type.unassign_type(self.file, [element]) + + # Handle containers and aggregates. + if ifcopenshell.util.element.get_container(element): + ifcopenshell.api.spatial.unassign_container(self.file, [element]) + elif ifcopenshell.util.element.get_aggregate(element): + ifcopenshell.api.aggregate.unassign_object(self.file, [element]) + + for rel in element.IsDefinedBy: + pset: ifcopenshell.entity_instance = rel.RelatingPropertyDefinition + objs = list(rel.RelatedObjects) + objs.remove(element) + if objs: + rel.RelatedObjects = objs + else: + history = getattr(rel, "OwnerHistory", None) + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) + psets_to_reassign.append(pset) + + psets = ifcopenshell.util.element.get_psets(element) + for pset_name in psets: + pset = self.file.by_id(psets[pset_name]["id"]) + + element = self.reassign_class(element, ifc_class, predefined_type) + + # Reassign psets. + if psets_to_reassign: + element.HasPropertySets = psets_to_reassign + + # Reassign representations. + for rep in representations: + ifcopenshell.api.geometry.assign_representation(self.file, product=element, representation=rep) + + # Keep IFC valid. + if switch_type == "type_to_occurrence" and representations: + ifcopenshell.api.geometry.edit_object_placement(self.file, product=element) + + return element + + def simple_reassignment( + self, + element: ifcopenshell.entity_instance, + ifc_class: str, + predefined_type: Union[str, None], + ) -> ifcopenshell.entity_instance: + element = self.reassign_class(element, ifc_class, predefined_type) if element.is_a("IfcTypeProduct"): - for occurrence in ifcopenshell.util.element.get_types(element) or []: - ifc_class = ifcopenshell.util.type.get_applicable_entities(self.settings["ifc_class"])[0] - self.reassign_class(occurrence, ifc_class) + for occurrence in ifcopenshell.util.element.get_types(element): + ifc_class_ = ifcopenshell.util.type.get_applicable_entities(ifc_class)[0] + self.reassign_class(occurrence, ifc_class_, predefined_type) else: element_type = ifcopenshell.util.element.get_type(element) if element_type: - ifc_class = ifcopenshell.util.type.get_applicable_types(self.settings["ifc_class"]) - if ifc_class and len(ifc_class) == 1: - element_type = self.reassign_class(element_type, ifc_class[0]) + ifc_class_ = next(iter(ifcopenshell.util.type.get_applicable_types(ifc_class))) + element_type = self.reassign_class(element_type, ifc_class_, predefined_type) ifc_class = element.is_a() - for occurrence in ifcopenshell.util.element.get_types(element_type) or []: + for occurrence in ifcopenshell.util.element.get_types(element_type): if occurrence == element: continue - self.reassign_class(occurrence, ifc_class) + self.reassign_class(occurrence, ifc_class, predefined_type) return element - def reassign_class(self, element, ifc_class): + def reassign_class( + self, element: ifcopenshell.entity_instance, ifc_class: str, predefined_type: Union[str, None] + ) -> ifcopenshell.entity_instance: element = ifcopenshell.util.schema.reassign_class(self.file, element, ifc_class) - if self.settings["predefined_type"] and hasattr(element, "PredefinedType"): + if predefined_type and hasattr(element, "PredefinedType"): try: - element.PredefinedType = self.settings["predefined_type"] + element.PredefinedType = predefined_type except: # PredefinedType wasn't in the respective enum, assume it's actually USERDEFINED # and set .ElementType / .ObjectType to the provided predefined type element.PredefinedType = "USERDEFINED" if element.is_a("IfcTypeProduct"): - element.ElementType = self.settings["predefined_type"] + element.ElementType = predefined_type else: - element.ObjectType = self.settings["predefined_type"] + element.ObjectType = predefined_type return element diff --git a/src/ifcopenshell-python/test/api/root/test_reassign_class.py b/src/ifcopenshell-python/test/api/root/test_reassign_class.py index 196a60f154..f7b1024b6b 100644 --- a/src/ifcopenshell-python/test/api/root/test_reassign_class.py +++ b/src/ifcopenshell-python/test/api/root/test_reassign_class.py @@ -16,9 +16,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell.api.aggregate +import ifcopenshell.api.geometry +import ifcopenshell.api.spatial +import ifcopenshell.util.representation import test.bootstrap -import ifcopenshell.api.type +import ifcopenshell.api.pset import ifcopenshell.api.root +import ifcopenshell.api.type import ifcopenshell.util.element @@ -95,6 +100,102 @@ class TestReassignClass(test.bootstrap.IFC4): assert len(self.file.by_type("IfcWall")) == 0 assert len(self.file.by_type("IfcWallType")) == 0 + # Switching between occurrence / type classes. + def test_unassign_type_from_occurrences_if_switching_from_type_class_to_occurrence_class(self): + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element1], relating_type=element_type) + element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element2], relating_type=element_type) + + element_type = ifcopenshell.api.root.reassign_class(self.file, product=element_type, ifc_class="IfcSlab") + + # Type is unassigned. + assert ifcopenshell.util.element.get_type(element1) is None + assert ifcopenshell.util.element.get_type(element2) is None + assert len(self.file.by_type("IfcRelDefinesByType")) == 0 + + assert len(self.file.by_type("IfcWall")) == 2 + assert len(self.file.by_type("IfcSlab")) == 1 + + def test_unassign_type_from_element_if_switching_from_occurrence_class_to_type_class(self): + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type) + + element = ifcopenshell.api.root.reassign_class(self.file, product=element, ifc_class="IfcSlabType") + + # Type is unassigned. + assert ifcopenshell.util.element.get_types(element_type) == [] + assert len(self.file.by_type("IfcRelDefinesByType")) == 0 + + assert len(self.file.by_type("IfcWallType")) == 1 + assert len(self.file.by_type("IfcSlabType")) == 1 + + def test_unassigning_container_switching_from_occurrence_class_to_type_class(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + container = self.file.create_entity("IfcBuilding") + ifcopenshell.api.spatial.assign_container(self.file, products=[element], relating_structure=container) + element = ifcopenshell.api.root.reassign_class(self.file, product=element, ifc_class="IfcSlabType") + + assert len(self.file.by_type("IfcRelContainedInSpatialStructure")) == 0 + assert len(self.file.by_type("IfcWall")) == 0 + assert len(self.file.by_type("IfcSlabType")) == 1 + + def test_unassigning_aggregate_switching_from_occurrence_class_to_type_class(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + aggregate = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcSlab") + ifcopenshell.api.aggregate.assign_object(self.file, products=[element], relating_object=aggregate) + element = ifcopenshell.api.root.reassign_class(self.file, product=element, ifc_class="IfcSlabType") + + assert len(self.file.by_type("IfcRelAggregates")) == 0 + assert len(self.file.by_type("IfcWall")) == 0 + assert len(self.file.by_type("IfcSlabType")) == 1 + + def test_keeping_psets_switching_from_occurrence_class_to_type_class(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + pset = ifcopenshell.api.pset.add_pset(self.file, element, name="TestPset") + element = ifcopenshell.api.root.reassign_class(self.file, product=element, ifc_class="IfcSlabType") + + assert ifcopenshell.util.element.get_pset(element, name="TestPset")["id"] == pset.id() + assert len(self.file.by_type("IfcRelDefinesByProperties")) == 0 + assert len(self.file.by_type("IfcWall")) == 0 + assert len(self.file.by_type("IfcSlabType")) == 1 + + def test_keeping_psets_switching_from_type_class_to_occurrence_class(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + pset = ifcopenshell.api.pset.add_pset(self.file, element, name="TestPset") + element = ifcopenshell.api.root.reassign_class(self.file, product=element, ifc_class="IfcSlab") + + assert ifcopenshell.util.element.get_pset(element, name="TestPset")["id"] == pset.id() + assert len(self.file.by_type("IfcRelDefinesByProperties")) == 1 + assert len(self.file.by_type("IfcWallType")) == 0 + assert len(self.file.by_type("IfcSlab")) == 1 + + def test_keeping_representations_switching_from_occurrence_class_to_type_class(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + context = self.file.create_entity("IfcGeometricRepresentationContext") + representation = self.file.create_entity("IfcShapeRepresentation", ContextOfItems=context) + ifcopenshell.api.geometry.assign_representation(self.file, element, representation) + element = ifcopenshell.api.root.reassign_class(self.file, product=element, ifc_class="IfcSlabType") + + assert ifcopenshell.util.representation.get_representation(element, context=context) == representation + assert len(self.file.by_type("IfcWall")) == 0 + assert len(self.file.by_type("IfcSlabType")) == 1 + + def test_keeping_representations_switching_from_type_class_to_occurrence_class(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + context = self.file.create_entity("IfcGeometricRepresentationContext") + representation = self.file.create_entity("IfcShapeRepresentation", ContextOfItems=context) + representation.Items = [self.file.create_entity("IfcExtrudedAreaSolid")] + ifcopenshell.api.geometry.assign_representation(self.file, element, representation) + element = ifcopenshell.api.root.reassign_class(self.file, product=element, ifc_class="IfcSlab") + + assert len(self.file.by_type("IfcRepresentationMap")) == 0 + assert ifcopenshell.util.representation.get_representation(element, context=context) == representation + assert len(self.file.by_type("IfcWallType")) == 0 + assert len(self.file.by_type("IfcSlab")) == 1 + class TestReassignClassIFC2X3(test.bootstrap.IFC2X3, TestReassignClass): pass