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