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
This commit is contained in:
Andrej730
2024-08-30 16:25:34 +05:00
parent acbdc0ed4c
commit bea837299e
5 changed files with 266 additions and 57 deletions
@@ -17,10 +17,16 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -16,9 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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