diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/pset/__init__.py index 73506f83c4..90333d164a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/__init__.py @@ -26,9 +26,11 @@ For example, if a door has a fire rating, it is stored as a property. from .. import wrap_usecases from .add_pset import add_pset from .add_qto import add_qto +from .assign_pset import assign_pset from .edit_pset import edit_pset from .edit_qto import edit_qto from .remove_pset import remove_pset +from .unassign_pset import unassign_pset from .unshare_pset import unshare_pset wrap_usecases(__path__, __name__) @@ -36,8 +38,10 @@ wrap_usecases(__path__, __name__) __all__ = [ "add_pset", "add_qto", + "assign_pset", "edit_pset", "edit_qto", "remove_pset", + "unassign_pset", "unshare_pset", ] diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py index 2afbd4b3af..ffa31faa8e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py @@ -18,6 +18,7 @@ import ifcopenshell import ifcopenshell.api.owner +import ifcopenshell.api.pset import ifcopenshell.guid from typing import Optional @@ -107,16 +108,9 @@ def add_pset( "Name": settings["name"], }, ) - file.create_entity( - "IfcRelDefinesByProperties", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": [settings["product"]], - "RelatingPropertyDefinition": pset, - }, - ) + ifcopenshell.api.pset.assign_pset(file, [settings["product"]], pset) return pset + elif settings["product"].is_a("IfcTypeObject"): for definition in settings["product"].HasPropertySets or []: if definition.Name == settings["name"]: @@ -130,10 +124,9 @@ def add_pset( "Name": settings["name"], }, ) - has_property_sets = list(settings["product"].HasPropertySets or []) - has_property_sets.append(pset) - settings["product"].HasPropertySets = has_property_sets + ifcopenshell.api.pset.assign_pset(file, [settings["product"]], pset) return pset + # in IFC2X3 IfcMaterialDefinition not yet existed elif settings["product"].is_a("IfcMaterialDefinition") or settings["product"].is_a("IfcMaterial"): kwargs = {"Material": settings["product"]} diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/assign_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/assign_pset.py new file mode 100644 index 0000000000..028f49f05f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/assign_pset.py @@ -0,0 +1,94 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell +import ifcopenshell.api.owner +import ifcopenshell.guid +import ifcopenshell.util.element +from typing import Union + + +def assign_pset( + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + pset: ifcopenshell.entity_instance, +) -> Union[ifcopenshell.entity_instance, None]: + """Assign property set to provided elements. + + This method can be used to make psets shared by multiple elements. + + :param products: Elements (or element types) to assign the pset to. + :param pset: Property set. + :return: None if `products` is empty or has only type elements. + IfcRelDefinesByProperties if `products` contains occurrences. + + Example: + + .. code:: python + + element = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") + ifcopenshell.api.pset.assign_pset(model, [element], pset) + # Pset is now assigned. + assert ifcopenshell.util.element.get_elements_using_pset(pset) == {element} + + element1 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") + element2 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall") + ifcopenshell.api.pset.assign_pset(model, [element1, element2], pset) + # Pset is now shared by multiple elements. + assert ifcopenshell.util.element.get_elements_using_pset(pset) == {element, element1, element2} + + # Same for element types. + element_type = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWallType") + ifcopenshell.api.pset.assign_pset(model, [element_type], type_pset) + # Pset is now assigned to the type. + assert ifcopenshell.util.element.get_elements_using_pset(type_pset) == {element_type} + """ + is_ifc2x3 = file.schema == "IFC2X3" + + products_occurrences: set[ifcopenshell.entity_instance] = set() + products_types: set[ifcopenshell.entity_instance] = set() + for product in products: + if product.is_a("IfcTypeProduct"): + products_types.add(product) + else: + products_occurrences.add(product) + + rel = None + # Check occurrences using pset. + if products_occurrences: + rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence + rel = next(iter(rels), None) + if rel is not None: + objs = set(rel.RelatedObjects) | products_occurrences + rel.RelatedObjects = list(objs) + else: + rel = file.create_entity( + "IfcRelDefinesByProperties", + **{ + "GlobalId": ifcopenshell.guid.new(), + "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), + "RelatedObjects": list(products_occurrences), + "RelatingPropertyDefinition": pset, + }, + ) + + for product in products_types: + psets = list(product.HasPropertySets or []) + product.HasPropertySets = psets + [pset] + + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/unassign_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/unassign_pset.py new file mode 100644 index 0000000000..bcd78c79ae --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/unassign_pset.py @@ -0,0 +1,80 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell +import ifcopenshell.api.owner +import ifcopenshell.guid +import ifcopenshell.util.element + + +def unassign_pset( + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + pset: ifcopenshell.entity_instance, +) -> None: + """Unassign property set from the provided elements. + + :param products: Elements (or element types) to assign the pset from. + :param pset: Property set. + + Example: + + .. code:: python + + element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.pset.assign_pset(self.file, [element1, element2], pset) + + # Pset is now shared by 2 elements. + assert ifcopenshell.util.element.get_elements_using_pset(pset) == {element1, element2} + + ifcopenshell.api.pset.unassign_pset(self.file, [element2], pset) + # Pset was unassigned from element2. + assert ifcopenshell.util.element.get_elements_using_pset(pset) == {element1} + + """ + is_ifc2x3 = file.schema == "IFC2X3" + + products_occurrences: set[ifcopenshell.entity_instance] = set() + products_types: set[ifcopenshell.entity_instance] = set() + for product in products: + if product.is_a("IfcTypeProduct"): + products_types.add(product) + else: + products_occurrences.add(product) + + # Check occurrences using pset. + if products_occurrences: + rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence + for rel in rels: + objs = set(rel.RelatedObjects) + if not any(p in objs for p in products_occurrences): + continue + objs.difference_update(products_occurrences) + if objs: + rel.RelatedObjects = list(objs) + else: + history = rel.OwnerHistory + file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(file, history) + + for product in products_types: + psets = list(product.HasPropertySets) + psets.remove(pset) + product.HasPropertySets = psets or None diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py index a788b0ec61..c18458228f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py @@ -18,6 +18,7 @@ import ifcopenshell import ifcopenshell.api.owner +import ifcopenshell.api.pset import ifcopenshell.guid import ifcopenshell.util.element @@ -34,8 +35,28 @@ def unshare_pset( :param products: Elements (or element types) to link the pset to. :param pset: Shared property set. :return: List of copied property sets. + + Example: + + .. code:: python + + element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.pset.assign_pset(self.file, [element1, element2], pset) + + # Pset is now shared by 2 elements. + assert ifcopenshell.util.element.get_elements_using_pset(pset) == {element1, element2} + + new_psets = ifcopenshell.api.pset.unshare_pset(self.file, [element2], pset) + + # element2 was unassigned from the original pset. + assert ifcopenshell.util.element.get_elements_using_pset(pset) == {element1} + new_pset = new_psets[0] + + # New pset was created and was assigned to element2. + assert new_pset != pset + assert ifcopenshell.util.element.get_elements_using_pset(new_pset) == {element2} """ - is_ifc2x3 = file.schema == "IFC2X3" products_occurrences: set[ifcopenshell.entity_instance] = set() products_types: set[ifcopenshell.entity_instance] = set() for product in products: @@ -44,51 +65,12 @@ def unshare_pset( else: products_occurrences.add(product) - # Check occurrences using pset. - rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence - for rel in rels: - objs = set(rel.RelatedObjects) - if not any(p in objs for p in products_occurrences): - continue - objs.difference_update(products_occurrences) - if objs: - rel.RelatedObjects = list(objs) - else: - history = rel.OwnerHistory - file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(file, history) - - # Check types using pset. - for product in products_types: - if not (psets := product.HasPropertySets): - continue - psets: list[ifcopenshell.entity_instance] = list(psets) - psets.remove(pset) - product.HasPropertySets = psets - - def assign_pset(product: ifcopenshell.entity_instance, pset: ifcopenshell.entity_instance) -> None: - if product.is_a("IfcTypeProduct"): - psets = list(product.HasPropertySets or []) - if not psets: - psets = [] - product.HasPropertySets = psets + [pset] - return - - file.create_entity( - "IfcRelDefinesByProperties", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": [product], - "RelatingPropertyDefinition": pset_copy, - }, - ) + ifcopenshell.api.pset.unassign_pset(file, products, pset) pset_copies: list[ifcopenshell.entity_instance] = [] for product in products: pset_copy = ifcopenshell.util.element.copy_deep(file, pset) pset_copies.append(pset_copy) - assign_pset(product, pset_copy) + ifcopenshell.api.pset.assign_pset(file, [product], pset_copy) return pset_copies diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py index 4eca742939..e9ac4b6891 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.aggregate import ifcopenshell.api.geometry +import ifcopenshell.api.pset import ifcopenshell.api.spatial import ifcopenshell.api.type import ifcopenshell.guid @@ -126,18 +127,12 @@ class Usecase: if switch_type == "type_to_occurrence": occurrences = ifcopenshell.util.element.get_types(element) + # Don't need to reassign as psets are linked to type directly, without rel. 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. @@ -150,28 +145,17 @@ class Usecase: 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"]) + psets_to_reassign.append(pset) + ifcopenshell.api.pset.unassign_pset(self.file, [element], pset) element = self.reassign_class(element, ifc_class, predefined_type) - # Reassign psets. - if psets_to_reassign: - element.HasPropertySets = psets_to_reassign + # Reassign psets. + for pset in psets_to_reassign: + ifcopenshell.api.pset.assign_pset(self.file, [element], pset) # Reassign representations. for rep in representations: diff --git a/src/ifcopenshell-python/test/api/pset/test_assign_pset.py b/src/ifcopenshell-python/test/api/pset/test_assign_pset.py new file mode 100644 index 0000000000..950d2743cb --- /dev/null +++ b/src/ifcopenshell-python/test/api/pset/test_assign_pset.py @@ -0,0 +1,58 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api.pset +import ifcopenshell.util.element + + +class TestAssignPset(test.bootstrap.IFC4): + def test_assign_pset_to_occurrence(self): + elements = [self.file.create_entity("IfcWall") for _ in range(3)] + pset = self.file.create_entity("IfcPropertySet") + rel = ifcopenshell.api.pset.assign_pset(self.file, elements, pset) + assert rel + + assert len(self.file.by_type("IfcRelDefinesByProperties")) == 1 + assert rel.RelatingPropertyDefinition == pset + assert set(rel.RelatedObjects) == set(elements) + + def test_assign_pset_to_occurrence_preexisting_rel(self): + elements = [self.file.create_entity("IfcWall") for _ in range(3)] + pset = self.file.create_entity("IfcPropertySet") + rel = ifcopenshell.api.pset.assign_pset(self.file, elements[:1], pset) + assert rel + + rel_updated = ifcopenshell.api.pset.assign_pset(self.file, elements[1:], pset) + assert rel_updated == rel + assert len(self.file.by_type("IfcRelDefinesByProperties")) == 1 + assert rel.RelatingPropertyDefinition == pset + assert set(rel.RelatedObjects) == set(elements) + + def test_assign_pset_to_type(self): + elements = [self.file.create_entity("IfcWallType") for _ in range(3)] + pset = self.file.create_entity("IfcPropertySet") + ret = ifcopenshell.api.pset.assign_pset(self.file, elements, pset) + assert ret is None + + assert len(self.file.by_type("IfcRelDefinesByProperties")) == 0 + assert set(pset.DefinesType) == set(elements) + + +class TestAssignPsetIFC2X3(test.bootstrap.IFC2X3, TestAssignPset): + pass diff --git a/src/ifcopenshell-python/test/api/pset/test_unassign_pset.py b/src/ifcopenshell-python/test/api/pset/test_unassign_pset.py new file mode 100644 index 0000000000..585a281713 --- /dev/null +++ b/src/ifcopenshell-python/test/api/pset/test_unassign_pset.py @@ -0,0 +1,66 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import test.bootstrap +import ifcopenshell.api.pset +import ifcopenshell.util.element + + +class TestUnassignPset(test.bootstrap.IFC4): + def test_unassign_pset_from_last_occurrence_and_remove_rel(self): + elements = [self.file.create_entity("IfcWall") for _ in range(3)] + pset = self.file.create_entity("IfcPropertySet") + ifcopenshell.api.pset.assign_pset(self.file, elements, pset) + + ifcopenshell.api.pset.unassign_pset(self.file, elements, pset) + assert len(self.file.by_type("IfcRelDefinesByProperties")) == 0 + + def test_unassign_pset_from_non_last_occurrence_and_reuse_rel(self): + elements = [self.file.create_entity("IfcWall") for _ in range(3)] + pset = self.file.create_entity("IfcPropertySet") + rel = ifcopenshell.api.pset.assign_pset(self.file, elements, pset) + assert rel + + ifcopenshell.api.pset.unassign_pset(self.file, elements[1:], pset) + assert rel.RelatedObjects == (elements[0],) + + def test_unassign_non_last_pset_from_type(self): + elements = [self.file.create_entity("IfcWallType") for _ in range(3)] + pset = self.file.create_entity("IfcPropertySet") + pset2 = self.file.create_entity("IfcPropertySet") + ifcopenshell.api.pset.assign_pset(self.file, elements, pset) + ifcopenshell.api.pset.assign_pset(self.file, elements, pset2) + + ifcopenshell.api.pset.unassign_pset(self.file, elements, pset) + assert pset.DefinesType == tuple() + assert set(pset2.DefinesType) == set(elements) + for element in elements: + assert element.HasPropertySets == (pset2,) + + def test_unassign_last_pset_from_type_and_set_prop_to_none(self): + elements = [self.file.create_entity("IfcWallType") for _ in range(3)] + pset = self.file.create_entity("IfcPropertySet") + ifcopenshell.api.pset.assign_pset(self.file, elements, pset) + ifcopenshell.api.pset.unassign_pset(self.file, elements, pset) + assert pset.DefinesType == tuple() + for element in elements: + assert element.HasPropertySets is None + + +class TestUnassignPsetIFC2X3(test.bootstrap.IFC2X3, TestUnassignPset): + pass diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 04d8da4a64..658b994c75 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -260,6 +260,18 @@ class TestGetPropertiesIFC4(test.bootstrap.IFC4): } +class TestGetElementsUsingPset(test.bootstrap.IFC4): + def test_run(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + pset = self.file.create_entity("IfcPropertySet") + ifcopenshell.api.pset.assign_pset(self.file, [element, element_type], pset) + assert subject.get_elements_using_pset(pset) == {element, element_type} + + +class TestGetElementsUsingPsetIFC2X3(test.bootstrap.IFC2X3, TestGetElementsUsingPset): ... + + class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4): def test_getting_an_element_predefined_type(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")