From 0e0861c84b5cfe08e77d649dfb926fb34d475da3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 15 Apr 2024 09:27:00 +1000 Subject: [PATCH 01/49] Further clarification of type vs occurrence psets/qtos. Previously even if you had a type selected it would still say occurrence. --- .../blenderbim/bim/module/material/ui.py | 2 +- src/blenderbim/blenderbim/bim/module/pset/data.py | 10 ++++++++++ src/blenderbim/blenderbim/bim/module/pset/ui.py | 14 ++++++++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 06a0fc04c3..1f24392af2 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -175,7 +175,7 @@ class BIM_PT_object_material(Panel): if ObjectMaterialData.data["type_material"]: row = self.layout.row(align=True) - row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT") + row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF") if ObjectMaterialData.data["material_class"]: return self.draw_material_ui() diff --git a/src/blenderbim/blenderbim/bim/module/pset/data.py b/src/blenderbim/blenderbim/bim/module/pset/data.py index 415ae1f5d9..0c3093f3a5 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/data.py +++ b/src/blenderbim/blenderbim/bim/module/pset/data.py @@ -69,6 +69,7 @@ class ObjectPsetsData(Data): @classmethod def load(cls): cls.data = { + "is_occurrence": cls.is_occurrence(), "psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), psets_only=True), "inherited_psets": cls.inherited_psets(), "pset_name": cls.pset_name(), @@ -76,6 +77,10 @@ class ObjectPsetsData(Data): } cls.is_loaded = True + @classmethod + def is_occurrence(cls): + return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject") + @classmethod def inherited_psets(cls): element = tool.Ifc.get_entity(bpy.context.active_object) @@ -126,11 +131,16 @@ class ObjectQtosData(Data): @classmethod def load(cls): cls.data = { + "is_occurrence": cls.is_occurrence(), "qtos": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), qtos_only=True), "inherited_qsets": cls.inherited_qsets(), } cls.is_loaded = True + @classmethod + def is_occurrence(cls): + return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject") + @classmethod def inherited_qsets(cls): element = tool.Ifc.get_entity(bpy.context.active_object) diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index 445016ba50..8a715e4718 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -208,12 +208,15 @@ class BIM_PT_object_psets(Panel): draw_psetqto_ui(context, 0, {}, props, self.layout, "Object") if ObjectPsetsData.data["psets"]: - self.layout.label(text="Instance:") + if ObjectPsetsData.data["is_occurrence"]: + self.layout.label(text="Occurrence Properties:") + else: + self.layout.label(text="Type Properties:") for pset in ObjectPsetsData.data["psets"]: draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object") if ObjectPsetsData.data["inherited_psets"]: - self.layout.label(text="Type:") + self.layout.label(text="Inherited Type Properties:", icon="CON_CHILDOF") for pset in ObjectPsetsData.data["inherited_psets"]: draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False) @@ -252,12 +255,15 @@ class BIM_PT_object_qtos(Panel): draw_psetqto_ui(context, 0, {}, props, self.layout, "Object") if ObjectQtosData.data["qtos"]: - self.layout.label(text="Instance:") + if ObjectQtosData.data["is_occurrence"]: + self.layout.label(text="Occurrence Quantities:") + else: + self.layout.label(text="Type Quantities:") for qto in ObjectQtosData.data["qtos"]: draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object") if ObjectQtosData.data["inherited_qsets"]: - self.layout.label(text="Type:") + self.layout.label(text="Inherited Type Quantities:", icon="CON_CHILDOF") for qset in ObjectQtosData.data["inherited_qsets"]: draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False) From 8a6255857595b01123a3b7a77a670217dd2758d4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 15 Apr 2024 09:27:58 +1000 Subject: [PATCH 02/49] Fix #4484. Clarify inheritance rules of predefined types. --- .../blenderbim/bim/module/root/data.py | 10 ++++++++++ src/blenderbim/blenderbim/bim/module/root/ui.py | 5 ++++- .../ifcopenshell/util/element.py | 17 +++++++++-------- .../test/util/test_element.py | 10 ++++++++++ 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/root/data.py b/src/blenderbim/blenderbim/bim/module/root/data.py index 6b91e18ab8..3ee83bb6db 100644 --- a/src/blenderbim/blenderbim/bim/module/root/data.py +++ b/src/blenderbim/blenderbim/bim/module/root/data.py @@ -42,6 +42,7 @@ class IfcClassData: cls.data["contexts"] = cls.contexts() cls.data["has_entity"] = cls.has_entity() cls.data["name"] = cls.name() + cls.data["has_inherited_predefined_type"] = cls.has_inherited_predefined_type() cls.data["ifc_class"] = cls.ifc_class() cls.data["ifc_predefined_types"] = cls.ifc_predefined_types() cls.data["can_reassign_class"] = cls.can_reassign_class() @@ -162,6 +163,15 @@ class IfcClassData: name += f"[{predefined_type}]" return name + @classmethod + def has_inherited_predefined_type(cls): + element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active) + if not element: + return + if element_type := ifcopenshell.util.element.get_type(element): + return ifcopenshell.util.element.get_predefined_type(element_type) != "NOTDEFINED" + return False + @classmethod def ifc_class(cls): element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active) diff --git a/src/blenderbim/blenderbim/bim/module/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py index b80c733ee5..a57bec2e40 100644 --- a/src/blenderbim/blenderbim/bim/module/root/ui.py +++ b/src/blenderbim/blenderbim/bim/module/root/ui.py @@ -61,7 +61,10 @@ class BIM_PT_class(Panel): self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN") else: row = self.layout.row(align=True) - row.label(text=IfcClassData.data["name"]) + row.label( + text=IfcClassData.data["name"], + icon="CON_CHILDOF" if IfcClassData.data["has_inherited_predefined_type"] else "NONE", + ) row.operator("bim.select_ifc_class", text="", icon="RESTRICT_SELECT_OFF") row.operator("bim.unlink_object", icon="UNLINKED", text="") if IfcClassData.data["can_reassign_class"]: diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 6c084b57ad..f5c788a8aa 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -26,10 +26,10 @@ def get_pset( element: ifcopenshell.entity_instance, name: str, prop: Optional[str] = None, - psets_only=False, - qtos_only=False, - should_inherit=True, - verbose=False, + psets_only: bool = False, + qtos_only: bool = False, + should_inherit: bool = True, + verbose: bool = False, ) -> Union[Any, dict[str, Any]]: """Retrieve a single property set or single property @@ -429,8 +429,7 @@ def get_predefined_type(element: ifcopenshell.entity_instance) -> str: element = ifcopenshell.by_type("IfcWall")[0] predefined_type = ifcopenshell.util.element.get_predefined_type(element) """ - element_type = get_type(element) - if element_type: + if element_type := get_type(element): predefined_type = getattr(element_type, "PredefinedType", None) if predefined_type == "USERDEFINED" or not predefined_type: predefined_type = getattr(element_type, "ElementType", ...) @@ -563,7 +562,9 @@ def get_material( return get_material(relating_type, should_skip_usage) -def get_materials(element: ifcopenshell.entity_instance, should_inherit=True) -> list[ifcopenshell.entity_instance]: +def get_materials( + element: ifcopenshell.entity_instance, should_inherit: bool = True +) -> list[ifcopenshell.entity_instance]: """Gets individual materials of an element If the element has a material set, the individual materials of that set are @@ -826,7 +827,7 @@ def get_layers( def get_container( - element: ifcopenshell.entity_instance, should_get_direct=False, ifc_class: Optional[str] = None + element: ifcopenshell.entity_instance, should_get_direct: bool = False, ifc_class: Optional[str] = None ) -> ifcopenshell.entity_instance: """ Retrieves the spatial structure container of an element. diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index c8a63e299d..9a038b044b 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -291,6 +291,16 @@ class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4): element_type.ProcessType = "FOOBAR" assert subject.get_predefined_type(element) == "FOOBAR" + def test_getting_an_element_type_predefined_type(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element_type.PredefinedType = "PARTITIONING" + assert subject.get_predefined_type(element_type) == "PARTITIONING" + + def test_getting_an_element_type_null_predefined_type(self): + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element_type.PredefinedType = "NOTDEFINED" + assert subject.get_predefined_type(element_type) == "NOTDEFINED" + class TestGetTypeIFC4(test.bootstrap.IFC4): def test_getting_the_type_of_a_product(self): From acf6628f23f48d4dff29327de324f7b13604b40e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 15 Apr 2024 11:53:37 +1000 Subject: [PATCH 03/49] Fix #4547. Handle both null inherited predefined types and disable editing predefined types on occurrences which inherit from a type. --- .../blenderbim/bim/module/attribute/operator.py | 17 +++++++++++++---- .../blenderbim/bim/module/root/data.py | 3 ++- .../api/attribute/edit_attributes.py | 3 ++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/attribute/operator.py b/src/blenderbim/blenderbim/bim/module/attribute/operator.py index 89b32597be..a86ff15e21 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/operator.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/operator.py @@ -47,10 +47,18 @@ class EnableEditingAttributes(bpy.types.Operator): obj = bpy.data.objects.get(self.obj) elif self.obj_type == "Material": obj = bpy.data.materials.get(self.obj) - oprops = obj.BIMObjectProperties props = obj.BIMAttributeProperties props.attributes.clear() + element = tool.Ifc.get_entity(obj) + has_inherited_predefined_type = False + if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)): + # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818 + has_inherited_predefined_type = ifcopenshell.util.element.get_predefined_type(element_type) not in ( + "NOTDEFINED", + None, + ) + def callback(name, prop, data): if name in ("RefLatitude", "RefLongitude"): new = props.attributes.add() @@ -62,10 +70,11 @@ class EnableEditingAttributes(bpy.types.Operator): new.string_value = "" if new.is_null else json.dumps(data[name]) blenderbim.bim.helper.add_attribute_description(new) new.description += " The degrees, minutes and seconds should follow this format : [12,34,56]" + if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type: + props.attributes.remove(len(props.attributes) - 1) + return True - blenderbim.bim.helper.import_attributes2( - tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback - ) + blenderbim.bim.helper.import_attributes2(element, props.attributes, callback=callback) props.is_editing_attributes = True return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/root/data.py b/src/blenderbim/blenderbim/bim/module/root/data.py index 3ee83bb6db..18f7fbffcd 100644 --- a/src/blenderbim/blenderbim/bim/module/root/data.py +++ b/src/blenderbim/blenderbim/bim/module/root/data.py @@ -169,7 +169,8 @@ class IfcClassData: if not element: return if element_type := ifcopenshell.util.element.get_type(element): - return ifcopenshell.util.element.get_predefined_type(element_type) != "NOTDEFINED" + # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818 + return ifcopenshell.util.element.get_predefined_type(element_type) not in ("NOTDEFINED", None) return False @classmethod diff --git a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py index 072427057a..42365ca802 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py +++ b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py @@ -64,7 +64,8 @@ class Usecase: self.settings["product"].PredefinedType = "USERDEFINED" elif hasattr(self.settings["product"], "ObjectType"): relating_type = ifcopenshell.util.element.get_type(self.settings["product"]) - if relating_type and relating_type.PredefinedType != "NOTDEFINED": + # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818 + if relating_type and relating_type.PredefinedType not in ("NOTDEFINED", None): self.settings["product"].ObjectType = None self.settings["product"].PredefinedType = None elif ( From 5e237a1670fcfc1ad7b4b55bc777f7a9e815ae8b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 15 Apr 2024 17:22:12 +1000 Subject: [PATCH 04/49] Fix #4553. --- src/blenderbim/blenderbim/tool/drawing.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index ca2552b17d..d8dcf606b8 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -733,7 +733,6 @@ class Drawing(blenderbim.core.tool.Drawing): obj.BIMObjectProperties.ifc_definition_id = ifc_definition_id - @classmethod def import_drawings(cls): props = bpy.context.scene.DocProperties @@ -1808,7 +1807,11 @@ class Drawing(blenderbim.core.tool.Drawing): # Note that render visibility is only set on drawing generation time for speed. obj.hide_set(False) - [obj.hide_set(False) for obj in bpy.context.view_layer.objects if obj.name not in element_obj_names] + [ + obj.hide_set(False) + for obj in bpy.context.view_layer.objects + if obj.name not in element_obj_names and tool.Ifc.get_entity(obj) + ] cls.import_camera_props(drawing, camera) From 919dcbe2d9332ba7782a661a222b45e3c9eba309 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 13:41:15 +0500 Subject: [PATCH 05/49] black format --- src/ifcopenshell-python/test/util/test_element.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 9a038b044b..d60849b1b2 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -362,12 +362,16 @@ class TestGetMaterial(test.bootstrap.IFC4): def test_getting_a_material_layer_set_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet" + ) assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_profile_set_of_a_product(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet") + rel = ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet" + ) assert subject.get_material(element) == rel.RelatingMaterial def test_getting_a_material_layer_set_usage_of_a_product(self): @@ -522,7 +526,9 @@ class TestGetElementsByMaterial(test.bootstrap.IFC4): material_set = ifcopenshell.api.run("material.add_material_set", self.file, set_type="IfcMaterialProfileSet") ifcopenshell.api.run("material.add_profile", self.file, profile_set=material_set, material=material) ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], material=material_set) - ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSetUsage") + ifcopenshell.api.run( + "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSetUsage" + ) usage = self.file.by_type("IfcMaterialProfileSetUsage")[0] assert subject.get_elements_by_material(self.file, material) == {element, element_type} assert subject.get_elements_by_material(self.file, material_set) == {element, element_type} From 0a9c9f2a284f03ed87ac6698e567414eb3af65da Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 11:24:25 +0500 Subject: [PATCH 06/49] typing --- .../blenderbim/bim/module/classification/operator.py | 1 + .../ifcopenshell/api/classification/add_classification.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index 84f2f89599..08020cef76 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -20,6 +20,7 @@ import bpy import json import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py index 0682ab7565..d59a4e9343 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py @@ -19,10 +19,11 @@ import ifcopenshell import ifcopenshell.util.schema import ifcopenshell.util.date +from typing import Union class Usecase: - def __init__(self, file, classification=None): + def __init__(self, file: ifcopenshell.file, classification: Union[str, ifcopenshell.entity_instance]): """Adds a new classification system to the project External classification systems such as Uniclass or Omniclass are @@ -81,7 +82,7 @@ class Usecase: "classification": classification, } - def execute(self): + def execute(self) -> ifcopenshell.entity_instance: if isinstance(self.settings["classification"], str): classification = self.file.createIfcClassification(Name=self.settings["classification"]) self.relate_to_project(classification) From 01ddf8e7699ade0817a20d2f1bd5ae60d4f0ec78 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 11:21:10 +0500 Subject: [PATCH 07/49] util.classification small optimization --- .../ifcopenshell/util/classification.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/classification.py b/src/ifcopenshell-python/ifcopenshell/util/classification.py index 12e82139cb..42208a90e7 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/classification.py +++ b/src/ifcopenshell-python/ifcopenshell/util/classification.py @@ -23,10 +23,10 @@ from typing import Optional def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -> set[ifcopenshell.entity_instance]: results = set() if not element.is_a("IfcRoot"): - if hasattr(element, "HasExternalReferences"): - return {r.RelatingReference for r in element.HasExternalReferences or []} - elif hasattr(element, "HasExternalReference"): # Seriously, IFC? - return {r.RelatingReference for r in element.HasExternalReference or []} + if (references := getattr(element, "HasExternalReferences", None)) is not None or ( + references := getattr(element, "HasExternalReference", None) + ) is not None: + return {r.RelatingReference for r in references} if should_inherit: element_type = ifcopenshell.util.element.get_type(element) if element_type and element_type != element: From 75c854da01778b82da01105d05363f3d98121cbe Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 13:29:47 +0500 Subject: [PATCH 08/49] ifcopenshell.util.get_referenced_elements --- .../ifcopenshell/util/element.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index f5c788a8aa..5a97494737 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -20,6 +20,7 @@ from __future__ import annotations import ifcopenshell import ifcopenshell.util.element from typing import Any, Callable, Optional, Union, Literal, overload +from collections import namedtuple def get_pset( @@ -1093,6 +1094,66 @@ def get_components(element: ifcopenshell.entity_instance, include_ports=False) - return is_decomposed_by[0].RelatedObjects +ReferenceData = namedtuple("ReferenceData", "inverse_attribute, rel_class, relating_element_attribute") + +# References below are omitted because they do not introduce +# any additional referenced objects besides the objects +# from their supertype IfcExternalReference +# - IfcExternallyDefinedHatchStyle +# - IfcExternallyDefinedSurfaceStyle +# - IfcExternallyDefinedTextFont + +REFERENCE_TYPES: dict[str, ReferenceData] = { + "IfcClassificationReference": ReferenceData( + "ClassificationRefForObjects", + "IfcRelAssociatesClassification", + "RelatingClassification", + ), + "IfcDocumentReference": ReferenceData("DocumentRefForObjects", "IfcRelAssociatesDocument", "RelatingDocument"), + "IfcLibraryReference": ReferenceData("LibraryRefForObjects", "IfcRelAssociatesLibrary", "RelatingLibrary"), +} + + +def get_referenced_elements(reference: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + """Get all elements with assigned `reference` + + :param reference: IfcExternalReference subtype reference + :type reference: ifcopenshell.entity_instance.entity_instance + :return: The elements with assigned `reference` + :rtype: set[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + reference = file.by_type("IfcClassificationReference")[0] + elements = ifcopenshell.util.element.get_referenced_elements(reference) + """ + + related_objects: set[ifcopenshell.entity_instance] = set() + ifc_file = reference.file + ifc_class = reference.is_a() + + if ifc_file.schema == "IFC2X3": + reference_data = REFERENCE_TYPES.get(ifc_class) + if reference_data: + for rel in ifc_file.by_type(reference_data.rel_class): + if getattr(rel, reference_data.relating_element_attribute) == reference: + related_objects.update(rel.RelatedObjects) + + else: + # IfcExternalReference + for external_rel in reference.ExternalReferenceForResources: + related_objects.update(external_rel.RelatedResourceObjects) + + reference_data = REFERENCE_TYPES.get(ifc_class) + if reference_data: + for rel in getattr(reference, reference_data.inverse_attribute): + related_objects.update(rel.RelatedObjects) + + return related_objects + + def replace_attribute(element: ifcopenshell.entity_instance, old: Any, new: Any) -> None: for i, attribute_value in enumerate(element): if has_element_reference(attribute_value, old): From 7d6fd03eb41a13432d5935d25c51f71933dd6f7c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 12:03:56 +0500 Subject: [PATCH 09/49] rename tests for consistency with api --- ...test_add_classification_reference.py => test_add_reference.py} | 0 ...emove_classification_reference.py => test_remove_reference.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/ifcopenshell-python/test/api/classification/{test_add_classification_reference.py => test_add_reference.py} (100%) rename src/ifcopenshell-python/test/api/classification/{test_remove_classification_reference.py => test_remove_reference.py} (100%) diff --git a/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py b/src/ifcopenshell-python/test/api/classification/test_add_reference.py similarity index 100% rename from src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py rename to src/ifcopenshell-python/test/api/classification/test_add_reference.py diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py similarity index 100% rename from src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py rename to src/ifcopenshell-python/test/api/classification/test_remove_reference.py From d6e69bd6e9cf2df269ad46221c9d4d9d36735210 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 12:06:39 +0500 Subject: [PATCH 10/49] classification.add_reference - support batching #4474 --- .../bim/module/classification/operator.py | 17 +- .../ifcopenshell/api/__init__.py | 3 + .../api/classification/add_reference.py | 147 +++++++++++------- .../api/classification/remove_reference.py | 2 +- .../api/classification/test_add_reference.py | 115 +++++++++----- .../test_remove_classification.py | 4 +- .../classification/test_remove_reference.py | 8 +- src/ifcopenshell-python/test/api/test_api.py | 33 ++++ .../test/util/test_classification.py | 14 +- .../test/util/test_selector.py | 2 +- src/ifctester/test/test_facet.py | 14 +- 11 files changed, 234 insertions(+), 125 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index 08020cef76..fa35886176 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -88,7 +88,7 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator): reference = ifcopenshell.api.run( "classification.add_reference", tool.Ifc.get(), - product=product, + products=[product], classification=classification, identification="X", name="Unnamed", @@ -358,15 +358,18 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator): classification = element break - for obj in objects: - ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) - if not ifc_definition_id: - continue + ifc_file = tool.Ifc.get() + products = [ + ifc_file.by_id(ifc_definition_id) + for obj in objects + if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)) + ] + if products: ifcopenshell.api.run( "classification.add_reference", tool.Ifc.get(), reference=IfcStore.classification_file.by_id(self.reference), - product=tool.Ifc.get().by_id(ifc_definition_id), + products=products, classification=classification, ) @@ -414,7 +417,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): reference = ifcopenshell.api.run( "classification.add_reference", tool.Ifc.get(), - product=element, + products=[element], classification=classification, identification=bsdd_classification.reference_code, name=bsdd_classification.name, diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 8f7b82351c..7c739cf71b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -87,6 +87,9 @@ ARGUMENTS_DEPRECATION = { "material.unassign_material": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "classification.add_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py index 320825e335..9057f7a25f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py @@ -17,12 +17,24 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element import ifcopenshell.util.schema +from typing import Optional, Union class Usecase: - def __init__(self, file, product=None, reference=None, identification=None, name=None, classification=None, is_lightweight=True): - """Adds a new classification reference and assigns it to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + reference: Optional[ifcopenshell.entity_instance] = None, + identification: Optional[str] = None, + name: Optional[str] = None, + classification: Optional[ifcopenshell.entity_instance] = None, + is_lightweight=True, + ): + """Adds a new classification reference and assigns it to the list of products A classification reference is a single entry such as "Pr_12_23_34" that is part of an external classification system (such as Uniclass or @@ -33,7 +45,7 @@ class Usecase: resources such as profiles, documents, libraries, and so on. Classification references can be added in two ways. Option 1) specify a - custom arbitrary reference, where you have the manually specify the + custom arbitrary reference, where you have to manually specify the identification (e.g. "Pr_12_23_45") and name (e.g. "Door Products"). Option 2) add a reference from an IFC classification library. The latter is preferred if you are using a common classification system such as @@ -52,13 +64,13 @@ class Usecase: assigned to both the type and an occurrence, then the assignment at the occurrence will override the type classification. - :param product: The IFC object, property, or resource you want to + :param product: The list of IFC objects, properties, or resources you want to associate the classification reference to. - :type product: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] :param reference: The classification reference entity taken from an IFC classification library. If you supply this parameter, you will use option 2. - :type product: ifcopenshell.entity_instance.entity_instance, optional + :type reference: ifcopenshell.entity_instance.entity_instance, optional :param identification: If you choose option 1 and do not specify a reference, you may manually specify an identification code. The code is typically a short identifier and may have punctuation to separate @@ -70,7 +82,7 @@ class Usecase: :param classification: The IfcClassification entity in your IFC model (not the library, if you are doing option 2) that the reference is part of. - :type product: ifcopenshell.entity_instance.entity_instance + :type classification: ifcopenshell.entity_instance.entity_instance :param is_lightweight: If you are doing option 2, choose whether or not to only add that particular reference (lighweight) or also add all of its parent references in the classification hierarchy (not @@ -81,8 +93,12 @@ class Usecase: is generally unnecessary. Using lightweight classifications are recommended and is the default. :type is_lightweight: bool, optional + + :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. + :return: The newly added IfcClassificationReference - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was empty list. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -93,7 +109,7 @@ class Usecase: classification = ifcopenshell.api.run("classification.add_classification", model, classification="MyCustomClassification") ifcopenshell.api.run("classification.add_reference", model, - product=wall_type, classification=classification, + products=[wall_type], classification=classification, identification="W_01", name="Interior Walls") # Option 2: adding a popular classification from a library @@ -104,12 +120,12 @@ class Usecase: reference = [r for r in library.by_type("IfcClassificationReference") if r.Identification == "XYZ"][0] ifcopenshell.api.run("classification.add_reference", model, - product=wall_type, classification=classification, + products=[wall_type], classification=classification, reference=reference) """ self.file = file self.settings = { - "product": product, + "products": products, "reference": reference, "identification": identification, "name": name, @@ -117,8 +133,27 @@ class Usecase: "is_lightweight": is_lightweight, } - def execute(self): - self.is_rooted = self.settings["product"].is_a("IfcRoot") + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + if not self.settings["products"]: + return + + if self.settings["reference"]: + referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + if set(self.settings["products"]).issubset(referenced): + # nothing to do, all elements already have this reference assigned + return self.settings["reference"] + + self.rooted_products: set[ifcopenshell.entity_instance] = set() + self.non_rooted_products: set[ifcopenshell.entity_instance] = set() + for product in self.settings["products"]: + if product.is_a("IfcRoot"): + self.rooted_products.add(product) + else: + self.non_rooted_products.add(product) + + if self.non_rooted_products and self.file.schema == "IFC2X3": + raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {self.non_rooted_products}.") + if self.settings["reference"]: return self.add_from_library() return self.add_from_identification() @@ -134,14 +169,10 @@ class Usecase: else: reference.Identification = self.settings["identification"] - relationship = self.get_existing_relationship(reference) - if relationship: - self.add_to_existing_relationship(relationship) - else: - self.add_new_relationship(reference) + self.update_relationships(reference) return reference - def add_from_library(self): + def add_from_library(self) -> ifcopenshell.entity_instance: if hasattr(self.settings["reference"], "ItemReference"): identification = self.settings["reference"].ItemReference # IFC2X3 else: @@ -155,8 +186,9 @@ class Usecase: old_referenced_source = self.settings["reference"].ReferencedSource self.settings["reference"].ReferencedSource = None else: + classification_name = self.settings["classification"].Name existing_classification = [ - c for c in self.file.by_type("IfcClassification") if c.Name == self.settings["classification"].Name + c for c in self.file.by_type("IfcClassification") if c.Name == classification_name ] reference = migrator.migrate(self.settings["reference"], self.file) @@ -174,15 +206,10 @@ class Usecase: for element in to_delete: self.file.remove(element) - relationship = self.get_existing_relationship(reference) - if relationship: - self.add_to_existing_relationship(relationship) - else: - self.add_new_relationship(reference) - + self.update_relationships(reference) return reference - def get_existing_reference(self, identification): + def get_existing_reference(self, identification: Optional[str] = None) -> Union[ifcopenshell.entity_instance, None]: for reference in self.file.by_type("IfcClassificationReference"): if self.file.schema == "IFC2X3": if reference.ItemReference == identification: @@ -191,39 +218,39 @@ class Usecase: if reference.Identification == identification: return reference - def add_new_relationship(self, reference): - if self.is_rooted: - self.file.create_entity( - "IfcRelAssociatesClassification", - GlobalId=ifcopenshell.guid.new(), - RelatedObjects=[self.settings["product"]], - RelatingClassification=reference, - ) - else: - self.file.create_entity( - "IfcExternalReferenceRelationship", - RelatingReference=reference, - RelatedResourceObjects=[self.settings["product"]], - ) - - def add_to_existing_relationship(self, rel): - if self.is_rooted: - related_objects = set(rel.RelatedObjects) - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) - else: - related_objects = set(rel.RelatedResourceObjects) - related_objects.add(self.settings["product"]) - rel.RelatedResourceObjects = list(related_objects) - - def get_existing_relationship(self, reference): - if self.is_rooted: + def update_relationships(self, reference: ifcopenshell.entity_instance) -> None: + root_rel, non_root_rel = None, None + if self.rooted_products: if self.file.schema == "IFC2X3": for rel in self.file.by_type("IfcRelAssociatesClassification"): if rel.RelatingClassification == reference: - return rel - elif reference.ClassificationRefForObjects: - return reference.ClassificationRefForObjects[0] - elif self.file.schema != "IFC2X3": - if reference.ExternalReferenceForResources: - return reference.ExternalReferenceForResources[0] + root_rel = rel + break + else: + root_rel = next(iter(reference.ClassificationRefForObjects), None) + + if root_rel: + related_objects = set(root_rel.RelatedObjects) | self.rooted_products + root_rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": root_rel}) + else: + self.file.create_entity( + "IfcRelAssociatesClassification", + OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), + GlobalId=ifcopenshell.guid.new(), + RelatedObjects=list(self.rooted_products), + RelatingClassification=reference, + ) + + if self.non_rooted_products: + # NOTE: Only Ifc4+. Ifc2x3 is already handled by raising TypeError + non_root_rel = next(iter(reference.ExternalReferenceForResources), None) + if non_root_rel: + related_objects = set(non_root_rel.RelatedResourceObjects) | self.non_rooted_products + non_root_rel.RelatedResourceObjects = list(related_objects) + else: + self.file.create_entity( + "IfcExternalReferenceRelationship", + RelatingReference=reference, + RelatedResourceObjects=list(self.non_rooted_products), + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py index 48b55b3740..b91989a1ae 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py @@ -44,7 +44,7 @@ class Usecase: classification = ifcopenshell.api.run("classification.add_classification", model, classification="MyCustomClassification") reference = ifcopenshell.api.run("classification.add_reference", model, - product=wall_type, classification=classification, + products=[wall_type], classification=classification, identification="W_01", name="Interior Walls") ifcopenshell.api.run("classification.remove_reference", model, reference=reference, product=wall_type) diff --git a/src/ifcopenshell-python/test/api/classification/test_add_reference.py b/src/ifcopenshell-python/test/api/classification/test_add_reference.py index 7cf5722b8f..ed52b05d35 100644 --- a/src/ifcopenshell-python/test/api/classification/test_add_reference.py +++ b/src/ifcopenshell-python/test/api/classification/test_add_reference.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import pytest import test.bootstrap import ifcopenshell.api import ifcopenshell.util.classification @@ -23,85 +24,127 @@ import ifcopenshell.util.classification class TestAddReference(test.bootstrap.IFC4): def test_adding_a_reference(self): + is_ifc2x3 = self.file.schema == "IFC2X3" + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element, element2], identification="X", name="Foobar", classification=result, ) references = list(ifcopenshell.util.classification.get_references(element)) assert len(references) == 1 - assert references[0].Identification == "X" + assert getattr(references[0], "ItemReference" if is_ifc2x3 else "Identification") == "X" assert references[0].Name == "Foobar" assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] - element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run( - "classification.add_reference", - self.file, - product=element2, - identification="X", - name="Foobar", - classification=result, + references2 = list(ifcopenshell.util.classification.get_references(element)) + assert len(references2) == 1 + assert getattr(references2[0], "ItemReference" if is_ifc2x3 else "Identification") == "X" + assert references2[0].Name == "Foobar" + assert references2[0] == references[0] + + rel = next( + rel + for rel in self.file.by_type("IfcRelAssociatesClassification") + if rel.RelatingClassification == references[0] ) - assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" - assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" - assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] + assert len(rel.RelatedObjects) == 2 def test_adding_a_library_based_reference(self): + is_ifc2x3 = self.file.schema == "IFC2X3" + library = ifcopenshell.file() classification = library.createIfcClassification(Name="Name") reference = library.createIfcClassificationReference(Identification="1", ReferencedSource=classification) ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification=classification) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element, element2], reference=reference, classification=result, ) references = list(ifcopenshell.util.classification.get_references(element)) assert len(references) == 1 - assert references[0].Identification == "1" + assert getattr(references[0], "ItemReference" if is_ifc2x3 else "Identification") == "1" assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] - def test_adding_a_reference_to_a_resource(self): + references2 = list(ifcopenshell.util.classification.get_references(element2)) + assert len(references2) == 1 + assert getattr(references2[0], "ItemReference" if is_ifc2x3 else "Identification") == "1" + assert references2[0].ReferencedSource == self.file.by_type("IfcClassification")[0] + assert references[0] == references2[0] + + rel = next( + rel + for rel in self.file.by_type("IfcRelAssociatesClassification") + if rel.RelatingClassification == references[0] + ) + assert len(rel.RelatedObjects) == 2 + + def test_adding_a_reference_to_a_resource_and_to_a_root(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = self.file.createIfcMaterial() + element2 = self.file.createIfcCostValue() + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") - ifcopenshell.api.run( - "classification.add_reference", - self.file, - product=element, - identification="X", - name="Foobar", - classification=result, - ) + + if self.file.schema == "IFC2X3": + with pytest.raises(TypeError): + ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + return + else: + ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + references = list(ifcopenshell.util.classification.get_references(element)) assert len(references) == 1 assert references[0].Identification == "X" assert references[0].Name == "Foobar" assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] - element2 = self.file.createIfcCostValue() - ifcopenshell.api.run( - "classification.add_reference", - self.file, - product=element2, - identification="X", - name="Foobar", - classification=result, - ) - assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" - assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" - assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] + references2 = list(ifcopenshell.util.classification.get_references(element2)) + assert references2[0].Identification == "X" + assert references2[0].Name == "Foobar" + assert references2[0] == references[0] + + references3 = list(ifcopenshell.util.classification.get_references(element3)) + assert references3[0].Identification == "X" + assert references3[0].Name == "Foobar" + assert references3[0] == references[0] assert len(self.file.by_type("IfcExternalReferenceRelationship")[0].RelatedResourceObjects) == 2 + rel = next( + rel + for rel in self.file.by_type("IfcRelAssociatesClassification") + if rel.RelatingClassification == references[0] + ) + assert len(rel.RelatedObjects) == 1 + + +class TestAddReferenceIFC2X3(test.bootstrap.IFC2X3, TestAddReference): + pass diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_classification.py b/src/ifcopenshell-python/test/api/classification/test_remove_classification.py index 3652d6e8c8..fb3537ddf2 100644 --- a/src/ifcopenshell-python/test/api/classification/test_remove_classification.py +++ b/src/ifcopenshell-python/test/api/classification/test_remove_classification.py @@ -34,7 +34,7 @@ class TestRemoveClassification(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, @@ -51,7 +51,7 @@ class TestRemoveClassification(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_reference.py b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py index 597317f3bb..63ae287aa3 100644 --- a/src/ifcopenshell-python/test/api/classification/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py @@ -29,7 +29,7 @@ class TestRemoveReference(test.bootstrap.IFC4): reference = ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, @@ -45,7 +45,7 @@ class TestRemoveReference(test.bootstrap.IFC4): reference = ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, @@ -62,7 +62,7 @@ class TestRemoveReference(test.bootstrap.IFC4): reference = ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, @@ -70,7 +70,7 @@ class TestRemoveReference(test.bootstrap.IFC4): reference2 = ifcopenshell.api.run( "classification.add_reference", self.file, - product=element2, + products=[element2], identification="X", name="Foobar", classification=result, diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 779d1238f2..7910cf9739 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.classification import ifcopenshell.util.element import ifcopenshell.util.system from datetime import datetime @@ -186,3 +187,35 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0 assert len(self.file.by_type("IfcWall")) == 1 assert len(self.file.by_type("IfcMaterial")) == 1 + + @deprecation_check + def test_adding_a_reference(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element, + identification="X", + name="Foobar", + classification=result, + ) + references = list(ifcopenshell.util.classification.get_references(element)) + assert len(references) == 1 + assert references[0].Identification == "X" + assert references[0].Name == "Foobar" + assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0] + + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "classification.add_reference", + self.file, + product=element2, + identification="X", + name="Foobar", + classification=result, + ) + assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" + assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" + assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] diff --git a/src/ifcopenshell-python/test/util/test_classification.py b/src/ifcopenshell-python/test/util/test_classification.py index d394f8056b..7fe054e1e3 100644 --- a/src/ifcopenshell-python/test/util/test_classification.py +++ b/src/ifcopenshell-python/test/util/test_classification.py @@ -34,14 +34,14 @@ class TestGetReferences(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], reference=reference1, classification=classification, ) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], reference=reference2, classification=classification, ) @@ -54,7 +54,7 @@ class TestGetReferences(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, @@ -74,14 +74,14 @@ class TestGetReferences(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], reference=reference1, classification=classification, ) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element_type, + products=[element_type], reference=reference2, classification=classification, ) @@ -103,14 +103,14 @@ class TestGetReferences(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], reference=reference1, classification=classification, ) ifcopenshell.api.run( "classification.add_reference", self.file, - product=element_type, + products=[element_type], reference=reference2, classification=classification, ) diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index d8ba4bb174..571511d39f 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -225,7 +225,7 @@ class TestFilterElements(test.bootstrap.IFC4): ifcopenshell.api.run( "classification.add_reference", self.file, - product=element, + products=[element], identification="X", name="Foobar", classification=result, diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py index bd89a00946..ddee91ab36 100644 --- a/src/ifctester/test/test_facet.py +++ b/src/ifctester/test/test_facet.py @@ -706,24 +706,24 @@ class TestClassification: element0 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") element1 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab") ifcopenshell.api.run( - "classification.add_reference", ifc, product=element1, reference=ref1, classification=system_a + "classification.add_reference", ifc, products=[element1], reference=ref1, classification=system_a ) element11 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcColumn") ifcopenshell.api.run( - "classification.add_reference", ifc, product=element11, reference=ref11, classification=system_a + "classification.add_reference", ifc, products=[element11], reference=ref11, classification=system_a ) element22 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam") ifcopenshell.api.run( "classification.add_reference", ifc, - product=element22, + products=[element22], reference=ref22, classification=system_a, is_lightweight=False, ) material = ifc.createIfcMaterial(Name="Material") ifcopenshell.api.run( - "classification.add_reference", ifc, product=material, reference=ref1, classification=system_a + "classification.add_reference", ifc, products=[material], reference=ref1, classification=system_a ) facet = Classification(system="Foobar") @@ -810,15 +810,15 @@ class TestClassification: wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType") ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type) ifcopenshell.api.run( - "classification.add_reference", ifc, product=wall, reference=ref11, classification=system_a + "classification.add_reference", ifc, products=[wall], reference=ref11, classification=system_a ) ifcopenshell.api.run( - "classification.add_reference", ifc, product=wall_type, reference=ref22, classification=system_a + "classification.add_reference", ifc, products=[wall_type], reference=ref22, classification=system_a ) system_b = ifcopenshell.api.run("classification.add_classification", ifc, classification=system_b) ifcopenshell.api.run( - "classification.add_reference", ifc, product=wall_type, reference=refx, classification=system_b + "classification.add_reference", ifc, products=[wall_type], reference=refx, classification=system_b ) facet = Classification(system="Foobar", value="11") From 144831f9ee0a2e3b53d060e43ec9bf92c6a321c2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 12:20:19 +0500 Subject: [PATCH 11/49] classification.remove_reference - support batching #4474 --- .../bim/module/classification/operator.py | 18 ++- .../ifcopenshell/api/__init__.py | 3 + .../api/classification/remove_reference.py | 110 ++++++++++++------ .../classification/test_remove_reference.py | 71 +++++++---- src/ifcopenshell-python/test/api/test_api.py | 17 +++ 5 files changed, 155 insertions(+), 64 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index fa35886176..74c84eae21 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -20,6 +20,7 @@ import bpy import json import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.classification import ifcopenshell.util.element import blenderbim.tool as tool import blenderbim.bim.helper @@ -293,6 +294,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator): active_reference = tool.Ifc.get().by_id(self.reference) identification = active_reference[1] + elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = [] for obj in objects: ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) @@ -301,12 +303,16 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator): if (identification and reference[1] == identification) or ( not identification and reference == active_reference ): - ifcopenshell.api.run( - "classification.remove_reference", - tool.Ifc.get(), - reference=reference, - product=element, - ) + elements_by_references.setdefault(reference, []).append(element) + + if elements_by_references: + for reference, products in elements_by_references.items(): + ifcopenshell.api.run( + "classification.remove_reference", + tool.Ifc.get(), + reference=reference, + products=products, + ) class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 7c739cf71b..d2027e06e1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -90,6 +90,9 @@ ARGUMENTS_DEPRECATION = { "classification.add_reference": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "classification.remove_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py index b91989a1ae..d4e8d802a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py @@ -17,12 +17,18 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: - def __init__(self, file, reference=None, product=None): - """Removes a classification reference from a product + def __init__( + self, + file: ifcopenshell.file, + reference: ifcopenshell.entity_instance, + products: list[ifcopenshell.entity_instance], + ): + """Removes a classification reference from the list of products If the classification reference is no longer associated to any products, the classification reference itself is also removed. @@ -30,9 +36,12 @@ class Usecase: :param reference: The IfcClassificationReference entity of the relationship you want to remove. :type reference: ifcopenshell.entity_instance.entity_instance - :param product: The object entity of the relationship you want to + :param product: The list fo object entities of the relationship you want to remove. - :type reference: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] + + :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. + :return: None :rtype: None @@ -47,39 +56,72 @@ class Usecase: products=[wall_type], classification=classification, identification="W_01", name="Interior Walls") ifcopenshell.api.run("classification.remove_reference", model, - reference=reference, product=wall_type) + reference=reference, products=[wall_type]) """ self.file = file - self.settings = {"reference": reference, "product": product} + self.settings = {"reference": reference, "products": products} - def execute(self): - if self.settings["product"].is_a("IfcRoot"): - for rel in self.file.by_type("IfcRelAssociatesClassification"): - if rel.RelatingClassification == self.settings["reference"] and rel.RelatedObjects: - if self.settings["product"] in rel.RelatedObjects: - related_objects = list(rel.RelatedObjects) - related_objects.remove(self.settings["product"]) - if len(related_objects): - rel.RelatedObjects = related_objects - else: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) - else: - for rel in self.file.by_type("IfcExternalReferenceRelationship"): - if rel.RelatingReference == self.settings["reference"] and rel.RelatedResourceObjects: - if self.settings["product"] in rel.RelatedResourceObjects: - related_objects = list(rel.RelatedResourceObjects) - related_objects.remove(self.settings["product"]) - if len(related_objects): - rel.RelatedResourceObjects = related_objects - else: - self.file.remove(rel) + def execute(self) -> None: + is_ifc2x3 = self.file.schema == "IFC2X3" + products = set(self.settings["products"]) + referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + products -= products.difference(referenced) + + # all products are already unassigned from a reference + if not products: + return + + rooted_products: set[ifcopenshell.entity_instance] = set() + non_rooted_products: set[ifcopenshell.entity_instance] = set() + for product in self.settings["products"]: + if product.is_a("IfcRoot"): + rooted_products.add(product) + else: + non_rooted_products.add(product) + + if non_rooted_products and is_ifc2x3: + raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {non_rooted_products}.") + + if rooted_products: + reference_rels: set[ifcopenshell.entity_instance] = set() + for product in rooted_products: + reference_rels.update(product.HasAssociations) + + reference_rels = { + rel + for rel in reference_rels + if rel.is_a("IfcRelAssociatesClassification") + and rel.RelatingClassification == self.settings["reference"] + } + + for rel in reference_rels: + related_objects = set(rel.RelatedObjects) - rooted_products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) + + if non_rooted_products: + reference_rels: set[ifcopenshell.entity_instance] = set() + for product in non_rooted_products: + rels = getattr(product, "HasExternalReferences", None) + if rels is None: + rels = getattr(product, "HasExternalReference", []) + reference_rels.update(rels) + + reference_rels = {rel for rel in reference_rels if rel.RelatingReference == self.settings["reference"]} + for rel in reference_rels: + related_objects = set(rel.RelatedResourceObjects) - non_rooted_products + if related_objects: + rel.RelatedResourceObjects = list(related_objects) + else: + self.file.remove(rel) # TODO: we only handle lightweight classifications here - if ( - not self.settings["reference"].ClassificationRefForObjects - and not self.settings["reference"].ExternalReferenceForResources - ): + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + if not referenced_elements: self.file.remove(self.settings["reference"]) diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_reference.py b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py index 63ae287aa3..8e03fdef4f 100644 --- a/src/ifcopenshell-python/test/api/classification/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import pytest import test.bootstrap import ifcopenshell.api import ifcopenshell.util.classification @@ -25,58 +26,80 @@ class TestRemoveReference(test.bootstrap.IFC4): def test_removing_a_reference(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") reference = ifcopenshell.api.run( "classification.add_reference", self.file, - products=[element], + products=[element, element2], identification="X", name="Foobar", classification=result, ) - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2], reference=reference + ) assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(ifcopenshell.util.classification.get_references(element2)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 - def test_removing_a_reference_from_a_resource(self): + def test_removing_a_reference_from_a_resource_and_from_a_root(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") element = self.file.createIfcMaterial() + element2 = self.file.createIfcCostValue() + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") - reference = ifcopenshell.api.run( - "classification.add_reference", - self.file, - products=[element], - identification="X", - name="Foobar", - classification=result, + if self.file.schema == "IFC2X3": + with pytest.raises(TypeError): + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + return + else: + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element, element2, element3], + identification="X", + name="Foobar", + classification=result, + ) + + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2, element3], reference=reference ) - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(ifcopenshell.util.classification.get_references(element2)) == 0 + assert len(ifcopenshell.util.classification.get_references(element3)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 def test_retaining_the_reference_if_still_in_use(self): ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") - element = self.file.createIfcMaterial() - element2 = self.file.createIfcMaterial() + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") reference = ifcopenshell.api.run( "classification.add_reference", self.file, - products=[element], - identification="X", - name="Foobar", - classification=result, - ) - reference2 = ifcopenshell.api.run( - "classification.add_reference", - self.file, - products=[element2], + products=[element, element2, element3], identification="X", name="Foobar", classification=result, ) assert len(self.file.by_type("IfcClassificationReference")) == 1 - ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + ifcopenshell.api.run( + "classification.remove_reference", self.file, products=[element, element2], reference=reference + ) assert len(self.file.by_type("IfcClassificationReference")) == 1 - ifcopenshell.api.run("classification.remove_reference", self.file, product=element2, reference=reference2) + ifcopenshell.api.run("classification.remove_reference", self.file, products=[element3], reference=reference) assert len(self.file.by_type("IfcClassificationReference")) == 0 + + +class TestRemoveReferenceIFC2X3(test.bootstrap.IFC2X3, TestRemoveReference): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 7910cf9739..0156bfa6ad 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -219,3 +219,20 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X" assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar" assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0] + + @deprecation_check + def test_removing_a_reference(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=[element], + identification="X", + name="Foobar", + classification=result, + ) + ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) + assert len(ifcopenshell.util.classification.get_references(element)) == 0 + assert len(self.file.by_type("IfcClassificationReference")) == 0 From 03af1013a0ee99e1c2af65633b2e325c2a2c92e5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 13:38:28 +0500 Subject: [PATCH 12/49] add tests for util.element.get_referenced_elements --- .../test/util/test_element.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index d60849b1b2..a29c849e76 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -785,6 +785,34 @@ class TestGetNestIFC2X3(test.bootstrap.IFC2X3, TestGetNestIFC4): pass +class TestGetReferencedElements(test.bootstrap.IFC4): + # TODO: test other references: + # IfcDocumentReference + # IfcLibraryReference + # IfcExternallyDefinedHatchStyle + # IfcExternallyDefinedSurfaceStyle + # IfcExternallyDefinedTextFont + def test_get_elements_referenced_by_classification_reference(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + elements = [ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")] + if self.file.schema != "IFC2X3": + elements.append(self.file.create_entity("IfcCostValue")) + result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name") + reference = ifcopenshell.api.run( + "classification.add_reference", + self.file, + products=elements, + identification="X", + name="Foobar", + classification=result, + ) + assert subject.get_referenced_elements(reference) == set(elements) + + +class TestGetReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedElements): + pass + + class TestReplaceAttributeIFC4(test.bootstrap.IFC4): def test_replacing_an_elements_attribute(self): element = self.file.createIfcWall("foo") From 81dcad6a146facf8b0579e68ebcd675902f7bb43 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 16:00:40 +0500 Subject: [PATCH 13/49] library.assign_reference - support batching #4474 --- src/blenderbim/blenderbim/core/brick.py | 2 +- src/blenderbim/blenderbim/core/library.py | 2 +- src/blenderbim/test/core/test_brick.py | 8 +-- src/blenderbim/test/core/test_library.py | 2 +- .../ifcopenshell/api/__init__.py | 3 + .../api/library/assign_reference.py | 63 +++++++++++-------- .../api/library/unassign_reference.py | 2 +- .../test/api/library/test_assign_reference.py | 57 ++++++++++------- .../test/api/library/test_remove_reference.py | 2 +- .../api/library/test_unassign_reference.py | 2 +- src/ifcopenshell-python/test/api/test_api.py | 10 +++ .../test/util/test_element.py | 10 ++- 12 files changed, 103 insertions(+), 60 deletions(-) diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py index c16b341b1c..58e7afed18 100644 --- a/src/blenderbim/blenderbim/core/brick.py +++ b/src/blenderbim/blenderbim/core/brick.py @@ -74,7 +74,7 @@ def assign_brick_reference(ifc, brick, element=None, library=None, brick_uri=Non if not reference: reference = ifc.run("library.add_reference", library=library) ifc.run("library.edit_reference", reference=reference, attributes=brick.export_brick_attributes(brick_uri)) - ifc.run("library.assign_reference", product=element, reference=reference) + ifc.run("library.assign_reference", products=[element], reference=reference) project = brick.get_brickifc_project() if not project: project = brick.add_brickifc_project(brick.get_namespace(brick_uri)) diff --git a/src/blenderbim/blenderbim/core/library.py b/src/blenderbim/blenderbim/core/library.py index eba4fc770a..c1103f5bf9 100644 --- a/src/blenderbim/blenderbim/core/library.py +++ b/src/blenderbim/blenderbim/core/library.py @@ -83,7 +83,7 @@ def edit_library_reference(ifc, library): def assign_library_reference(ifc, obj=None, reference=None): - ifc.run("library.assign_reference", product=ifc.get_entity(obj), reference=reference) + ifc.run("library.assign_reference", products=[ifc.get_entity(obj)], reference=reference) def unassign_library_reference(ifc, obj=None, reference=None): diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py index 157a363682..e410fbe9b9 100644 --- a/src/blenderbim/test/core/test_brick.py +++ b/src/blenderbim/test/core/test_brick.py @@ -124,21 +124,21 @@ class TestAssignBrickReference: ifc.run("library.add_reference", library="library").should_be_called().will_return("reference") brick.export_brick_attributes("brick_uri").should_be_called().will_return("attributes") ifc.run("library.edit_reference", reference="reference", attributes="attributes").should_be_called() - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_assigning_to_an_existing_reference(self, ifc, brick): brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return("project") brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called() subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri") def test_adding_a_brickifc_project_if_it_doesnt_exist(self, ifc, brick): brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference") - ifc.run("library.assign_reference", product="element", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called() brick.get_brickifc_project().should_be_called().will_return(None) brick.get_namespace("brick_uri").should_be_called().will_return("namespace") brick.add_brickifc_project("namespace").should_be_called().will_return("project") @@ -277,4 +277,4 @@ class TestSetBrickListRoot: class TestRemoveBrickRelation: def test_run(self, brick): brick.remove_relation("brick_uri", "predicate", "object").should_be_called() - subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") \ No newline at end of file + subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object") diff --git a/src/blenderbim/test/core/test_library.py b/src/blenderbim/test/core/test_library.py index 2809722e81..9e4f9ffee6 100644 --- a/src/blenderbim/test/core/test_library.py +++ b/src/blenderbim/test/core/test_library.py @@ -114,7 +114,7 @@ class TestEditLibraryReference: class TestAssignLibraryReference: def test_run(self, ifc): ifc.get_entity("obj").should_be_called().will_return("product") - ifc.run("library.assign_reference", product="product", reference="reference").should_be_called() + ifc.run("library.assign_reference", products=["product"], reference="reference").should_be_called() subject.assign_library_reference(ifc, obj="obj", reference="reference") diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index d2027e06e1..b9e60e65a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -93,6 +93,9 @@ ARGUMENTS_DEPRECATION = { "classification.remove_reference": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "library.assign_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py index a1f3f83d17..8ee4a21eb5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py @@ -17,23 +17,30 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, product=None, reference=None): - """Associates a product with a library reference + def __init__( + self, file: ifcopenshell.file, products: ifcopenshell.entity_instance, reference: ifcopenshell.entity_instance + ): + """Associates a list products with a library reference A product may be associated with zero, one, or many references across multiple libraries. See ifcopenshell.api.library.add_reference for more detail about how references work. - :param product: The IfcProduct you want to associate with the reference - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of IfcProducts you want to associate with the reference + :type products: list[ifcopenshell.entity_instance.entity_instance] :param reference: The IfcLibraryReference you want the product to be associated with. :type reference: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssociatesLibrary relationship entity - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was an empty list or all products were + already assigned to the `reference`. + :rtype: Union[ifcopenshell.entity_instance.entity_instance, None] Example: @@ -51,40 +58,46 @@ class Usecase: ifc_class="IfcUnitaryEquipment", predefined_type="AIRHANDLER") # And now assign the IFC model's AHU with its Brickschema counterpart - ifcopenshell.api.run("library.assign_reference", model, reference=reference, product=ahu) + ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu]) """ self.file = file self.settings = { - "product": product, + "products": products, "reference": reference, } - def execute(self): + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"]) + products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) + products = products - referenced_elements + + if not products: + return + if self.file.schema == "IFC2X3": - rels = self.get_ifc2x3_rels() + rel = next( + ( + r + for r in self.file.by_type("IfcRelAssociatesLibrary") + if r.RelatingLibrary == self.settings["reference"] + ), + None, + ) else: - rels = self.settings["reference"].LibraryRefForObjects - if not rels: + rel = next(iter(self.settings["reference"].LibraryRefForObjects), None) + + if not rel: return self.file.create_entity( "IfcRelAssociatesLibrary", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), - RelatedObjects=[self.settings["product"]], + RelatedObjects=list(products), RelatingLibrary=self.settings["reference"], ) - for rel in rels: - if self.settings["product"] in rel.RelatedObjects: - return rel - - rel = rels[0] - related_objects = list(rel.RelatedObjects) - related_objects.append(self.settings["product"]) - rel.RelatedObjects = related_objects + related_objects = set(rel.RelatedObjects) | products + rel.RelatedObjects = list(related_objects) ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel) return rel - - def get_ifc2x3_rels(self): - return [ - r for r in self.file.by_type("IfcRelAssociatesLibrary") if r.RelatingLibrary == self.settings["reference"] - ] diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py index 639a3dd35b..5bd04bc581 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py @@ -49,7 +49,7 @@ class Usecase: ifc_class="IfcUnitaryEquipment", predefined_type="AIRHANDLER") # And now assign the IFC model's AHU with its Brickschema counterpart - ifcopenshell.api.run("library.assign_reference", model, reference=reference, product=ahu) + ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu]) # Let's change our mind and unassign it. ifcopenshell.api.run("library.unassign_reference", model, reference=reference, product=ahu) diff --git a/src/ifcopenshell-python/test/api/library/test_assign_reference.py b/src/ifcopenshell-python/test/api/library/test_assign_reference.py index 1133b592d6..550df3940f 100644 --- a/src/ifcopenshell-python/test/api/library/test_assign_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_assign_reference.py @@ -20,39 +20,48 @@ import test.bootstrap import ifcopenshell.api +def validate_ifc_file(ifc_file: ifcopenshell.file, use_json=True): + import ifcopenshell + import ifcopenshell.validate + + if use_json: + logger = ifcopenshell.validate.json_logger() + else: + import logging + + logger = logging.getLogger("validate") + logger.setLevel(logging.DEBUG) + + ifcopenshell.validate.validate(ifc_file, logger, express_rules=True) + if use_json: + if logger.statements: + from pprint import pprint + + pprint(logger.statements) + else: + print("IFC is completely valid.") + + class TestAssignReference(test.bootstrap.IFC4): def test_assigning_a_reference(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() product2 = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) - ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product, product2) + product3 = self.file.createIfcWall() + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) + rel = self.file.by_type("IfcRelAssociatesLibrary")[0] + assert rel.RelatedObjects == (product,) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product2, product3], reference=reference) + assert set(rel.RelatedObjects) == set((product, product2, product3)) def test_not_assigning_twice(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) - - -class TestAssignReferenceIFC2X3(test.bootstrap.IFC2X3): - def test_assigning_a_reference(self): - reference = self.file.createIfcLibraryReference() - product = self.file.createIfcWall() - product2 = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) rel = self.file.by_type("IfcRelAssociatesLibrary")[0] assert rel.RelatedObjects == (product,) - ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) - assert rel.RelatedObjects == (product, product2) - def test_not_assigning_twice(self): - reference = self.file.createIfcLibraryReference() - product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) - rel = self.file.by_type("IfcRelAssociatesLibrary")[0] - assert rel.RelatedObjects == (product,) + +class TestAssignReferenceIFC2X3(test.bootstrap.IFC2X3, TestAssignReference): + pass diff --git a/src/ifcopenshell-python/test/api/library/test_remove_reference.py b/src/ifcopenshell-python/test/api/library/test_remove_reference.py index 26a9106411..a98ed2f083 100644 --- a/src/ifcopenshell-python/test/api/library/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_remove_reference.py @@ -24,7 +24,7 @@ class TestRemoveReference(test.bootstrap.IFC4): def test_removing_a_reference(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) ifcopenshell.api.run("library.remove_reference", self.file, reference=reference) assert len(self.file.by_type("IfcLibraryReference")) == 0 assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 diff --git a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py index 900dcdcc12..598022cbab 100644 --- a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py @@ -24,6 +24,6 @@ class TestUnassignReference(test.bootstrap.IFC4): def test_unassigning_a_reference(self): reference = self.file.createIfcLibraryReference() product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference) assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 0156bfa6ad..ee20435b33 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -236,3 +236,13 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference) assert len(ifcopenshell.util.classification.get_references(element)) == 0 assert len(self.file.by_type("IfcClassificationReference")) == 0 + + @deprecation_check + def test_assigning_a_reference(self): + reference = self.file.createIfcLibraryReference() + product = self.file.createIfcWall() + product2 = self.file.createIfcWall() + ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference) + assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) + ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) + assert set(reference.LibraryRefForObjects[0].RelatedObjects) == set((product, product2)) diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index a29c849e76..1a701f5ec9 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -788,7 +788,6 @@ class TestGetNestIFC2X3(test.bootstrap.IFC2X3, TestGetNestIFC4): class TestGetReferencedElements(test.bootstrap.IFC4): # TODO: test other references: # IfcDocumentReference - # IfcLibraryReference # IfcExternallyDefinedHatchStyle # IfcExternallyDefinedSurfaceStyle # IfcExternallyDefinedTextFont @@ -808,6 +807,15 @@ class TestGetReferencedElements(test.bootstrap.IFC4): ) assert subject.get_referenced_elements(reference) == set(elements) + def test_get_elements_referenced_by_library_reference(self): + reference = self.file.createIfcLibraryReference() + elements = [ + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ] + ifcopenshell.api.run("library.assign_reference", self.file, reference=reference, products=elements) + assert subject.get_referenced_elements(reference) == set(elements) + class TestGetReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedElements): pass From 1f605484d9c04b402e0b79098a507f45f0bcae45 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 16:28:43 +0500 Subject: [PATCH 14/49] library.unassign_reference - support batching #4474 --- src/blenderbim/blenderbim/core/library.py | 2 +- src/blenderbim/test/core/test_library.py | 2 +- .../ifcopenshell/api/__init__.py | 3 + .../api/library/unassign_reference.py | 55 ++++++++++++------- .../api/library/test_unassign_reference.py | 14 ++++- src/ifcopenshell-python/test/api/test_api.py | 8 +++ 6 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/blenderbim/blenderbim/core/library.py b/src/blenderbim/blenderbim/core/library.py index c1103f5bf9..6b5ce7e767 100644 --- a/src/blenderbim/blenderbim/core/library.py +++ b/src/blenderbim/blenderbim/core/library.py @@ -87,4 +87,4 @@ def assign_library_reference(ifc, obj=None, reference=None): def unassign_library_reference(ifc, obj=None, reference=None): - ifc.run("library.unassign_reference", product=ifc.get_entity(obj), reference=reference) + ifc.run("library.unassign_reference", products=[ifc.get_entity(obj)], reference=reference) diff --git a/src/blenderbim/test/core/test_library.py b/src/blenderbim/test/core/test_library.py index 9e4f9ffee6..4722cde5c0 100644 --- a/src/blenderbim/test/core/test_library.py +++ b/src/blenderbim/test/core/test_library.py @@ -121,5 +121,5 @@ class TestAssignLibraryReference: class TestUnassignLibraryReference: def test_run(self, ifc): ifc.get_entity("obj").should_be_called().will_return("product") - ifc.run("library.unassign_reference", product="product", reference="reference").should_be_called() + ifc.run("library.unassign_reference", products=["product"], reference="reference").should_be_called() subject.unassign_library_reference(ifc, obj="obj", reference="reference") diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index b9e60e65a6..6d27dbebfd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -96,6 +96,9 @@ ARGUMENTS_DEPRECATION = { "library.assign_reference": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "library.unassign_reference": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py index 5bd04bc581..b650ffba75 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py @@ -18,18 +18,24 @@ import ifcopenshell import ifcopenshell.util.element +import ifcopenshell.api class Usecase: - def __init__(self, file, reference=None, product=None): - """Unassigns a product from a reference + def __init__( + self, + file: ifcopenshell.file, + reference: ifcopenshell.entity_instance, + products: list[ifcopenshell.entity_instance], + ): + """Unassigns a product of products from a reference If the product isn't assigned to the reference, nothing will happen. :param reference: The IfcLibraryReference to unassign from :type reference: ifcopenshell.entity_instance.entity_instance - :param product: A IfcProduct element to unassign from the reference - :type product: ifcopenshell.entity_instance.entity_instance + :param products: A list of IfcProduct elements to unassign from the reference + :type products: list[ifcopenshell.entity_instance.entity_instance] :return: None :rtype: None @@ -52,24 +58,33 @@ class Usecase: ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu]) # Let's change our mind and unassign it. - ifcopenshell.api.run("library.unassign_reference", model, reference=reference, product=ahu) + ifcopenshell.api.run("library.unassign_reference", model, reference=reference, products=[ahu]) """ self.file = file - self.settings = {"reference": reference, "product": product} + self.settings = {"reference": reference, "products": products} def execute(self): - rels = self.settings["reference"].LibraryRefForObjects - if not rels: - return - for rel in rels: - if self.settings["product"] in rel.RelatedObjects: - if len(rel.RelatedObjects) == 1: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) - continue - related_objects = list(rel.RelatedObjects) - related_objects.remove(self.settings["product"]) - rel.RelatedObjects = related_objects + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + + reference_rels: set[ifcopenshell.entity_instance] = set() + products = set(self.settings["products"]) + for product in products: + reference_rels.update(product.HasAssociations) + + reference_rels = { + rel + for rel in reference_rels + if rel.is_a("IfcRelAssociatesLibrary") and rel.RelatingLibrary == self.settings["reference"] + } + + for rel in reference_rels: + related_objects = set(rel.RelatedObjects) - products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) diff --git a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py index 598022cbab..8962bb686a 100644 --- a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py +++ b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py @@ -18,12 +18,20 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestUnassignReference(test.bootstrap.IFC4): def test_unassigning_a_reference(self): reference = self.file.createIfcLibraryReference() - product = self.file.createIfcWall() - ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) - ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference) + products = [self.file.createIfcWall() for i in range(3)] + ifcopenshell.api.run("library.assign_reference", self.file, products=products, reference=reference) + ifcopenshell.api.run("library.unassign_reference", self.file, products=products[:1], reference=reference) + assert ifcopenshell.util.element.get_referenced_elements(reference) == set(products[1:]) + + ifcopenshell.api.run("library.unassign_reference", self.file, products=products[1:], reference=reference) + assert ifcopenshell.util.element.get_referenced_elements(reference) == set() assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 + +class TestUnassignReferenceIFC2X3(test.bootstrap.IFC2X3, TestUnassignReference): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index ee20435b33..a652a0de0c 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -246,3 +246,11 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): assert reference.LibraryRefForObjects[0].RelatedObjects == (product,) ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference) assert set(reference.LibraryRefForObjects[0].RelatedObjects) == set((product, product2)) + + @deprecation_check + def test_unassigning_a_reference(self): + reference = self.file.createIfcLibraryReference() + product = self.file.createIfcWall() + ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) + ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference) + assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 From 097f44284fb0aad79d43dec7f0ccb81fb77610da Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 16:32:54 +0500 Subject: [PATCH 15/49] document.assign_document - support batching #4474 --- src/blenderbim/blenderbim/core/document.py | 2 +- src/blenderbim/blenderbim/core/drawing.py | 4 +- src/blenderbim/test/core/test_document.py | 2 +- src/blenderbim/test/core/test_drawing.py | 4 +- .../ifcopenshell/api/__init__.py | 3 + .../api/document/assign_document.py | 85 ++++++++++++------- .../api/document/unassign_document.py | 2 +- .../test/api/document/test_assign_document.py | 15 ++-- .../api/document/test_remove_reference.py | 2 +- .../api/document/test_unassign_document.py | 6 +- src/ifcopenshell-python/test/api/test_api.py | 7 ++ .../test/util/test_element.py | 10 ++- 12 files changed, 91 insertions(+), 51 deletions(-) diff --git a/src/blenderbim/blenderbim/core/document.py b/src/blenderbim/blenderbim/core/document.py index ce6dc374c6..85cfc00681 100644 --- a/src/blenderbim/blenderbim/core/document.py +++ b/src/blenderbim/blenderbim/core/document.py @@ -109,7 +109,7 @@ def remove_document(ifc, document_tool, document=None): def assign_document(ifc, product=None, document=None): - ifc.run("document.assign_document", product=product, document=document) + ifc.run("document.assign_document", products=[product], document=document) def unassign_document(ifc, product=None, document=None): diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index f8039488f2..127cbfa7db 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -241,7 +241,7 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None): attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"} ifc.run("document.edit_information", information=information, attributes=attributes) ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) - ifc.run("document.assign_document", product=element, document=reference) + ifc.run("document.assign_document", products=[element], document=reference) drawing.import_drawings() @@ -276,7 +276,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"} ifc.run("document.edit_information", information=information, attributes=attributes) ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri}) - ifc.run("document.assign_document", product=new_drawing, document=reference) + ifc.run("document.assign_document", products=[new_drawing], document=reference) drawing_tool.import_drawings() return new_drawing diff --git a/src/blenderbim/test/core/test_document.py b/src/blenderbim/test/core/test_document.py index 708c899e48..2349870d39 100644 --- a/src/blenderbim/test/core/test_document.py +++ b/src/blenderbim/test/core/test_document.py @@ -133,7 +133,7 @@ class TestRemoveDocument: class TestAssignDocument: def test_run(self, ifc): - ifc.run("document.assign_document", product="product", document="document").should_be_called() + ifc.run("document.assign_document", products=["product"], document="document").should_be_called() subject.assign_document(ifc, product="product", document="document") diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index b07d0f8368..afe116d072 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -365,7 +365,7 @@ class TestAddDrawing: attributes={"Identification": "X", "Name": "name", "Scope": "DRAWING"}, ).should_be_called() ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called() - ifc.run("document.assign_document", product="element", document="reference").should_be_called() + ifc.run("document.assign_document", products=["element"], document="reference").should_be_called() drawing.import_drawings().should_be_called() subject.add_drawing(ifc, collector, drawing, target_view="target_view", location_hint="location_hint") @@ -405,7 +405,7 @@ class TestDuplicateDrawing: ifc.run( "document.edit_reference", reference="reference", attributes={"Location": "drawing_path"} ).should_be_called() - ifc.run("document.assign_document", product="new_drawing", document="reference").should_be_called() + ifc.run("document.assign_document", products=["new_drawing"], document="reference").should_be_called() drawing.import_drawings().should_be_called() subject.duplicate_drawing(ifc, drawing, drawing="drawing", should_duplicate_annotations=True) diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 6d27dbebfd..c82e076464 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -99,6 +99,9 @@ ARGUMENTS_DEPRECATION = { "library.unassign_reference": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "document.assign_document": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py index c7f98e4517..f67cbe890a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py @@ -17,11 +17,19 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api +import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, product=None, document=None): - """Assigns a document to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + document: ifcopenshell.entity_instance, + ): + """Assigns a document to a list of products An object may be assigned to zero, one, or multiple documents. Almost any object or property may be assigned to a document, though typically @@ -32,14 +40,16 @@ class Usecase: consistent with other external relationships (such as classification systems or libraries). - :param product: The object to associate the document to. This could be + :param product: The list of objects to associate the document to. This could be almost any sensible object in IFC. - :type product: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] :param document: The IfcDocumentReference to associate to, or alternatively an IfcDocumentInformation, though this is not recommended. :type document: ifcopenshell.entity_instance.entity_instance :return: The IfcRelAssociatesDocument relationship + or `None` if `products` was an empty list or all products were + already assigned to the `document`. :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -54,42 +64,51 @@ class Usecase: reference = ifcopenshell.api.run("document.add_reference", model, information=document) # Let's imagine storey represents an IfcBuildingStorey for the ground floor - ifcopenshell.api.run("document.assign_document", model, product=storey, document=reference) + ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference) """ self.file = file self.settings = { - "product": product, + "products": products, "document": document, } - def execute(self): - rel = self.get_document_rel() - related_objects = set(rel.RelatedObjects) if rel.RelatedObjects else set() - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + # NOTE: reuses code from `library.assign_reference` + + referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["document"]) + products: set[ifcopenshell.entity_instance] = set(self.settings["products"]) + products = products - referenced_elements + + if not products: + return - def get_document_rel(self): if self.file.schema == "IFC2X3": - for rel in self.file.by_type("IfcRelAssociatesDocument"): - if rel.RelatingDocument == self.settings["document"]: - return rel + rel = next( + ( + r + for r in self.file.by_type("IfcRelAssociatesDocument") + if r.RelatingDocument == self.settings["document"] + ), + None, + ) else: - if ( - hasattr(self.settings["document"], "DocumentRefForObjects") - and self.settings["document"].DocumentRefForObjects - ): - return self.settings["document"].DocumentRefForObjects[0] - elif ( - hasattr(self.settings["document"], "DocumentInfoForObjects") - and self.settings["document"].DocumentInfoForObjects - ): - return self.settings["document"].DocumentInfoForObjects[0] + ifc_class = self.settings["document"].is_a() + if ifc_class == "IfcDocumentReference": + rel = next(iter(self.settings["document"].DocumentRefForObjects), None) + elif ifc_class == "IfcDocumentInformation": + rel = next(iter(self.settings["document"].DocumentInfoForObjects), None) - return self.file.create_entity( - "IfcRelAssociatesDocument", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatingDocument": self.settings["document"], - } - ) + if not rel: + return self.file.create_entity( + "IfcRelAssociatesDocument", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), + RelatedObjects=list(products), + RelatingDocument=self.settings["document"], + ) + + related_objects = set(rel.RelatedObjects) | products + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel) + return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py index 5a08c13262..c4fbca8234 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py @@ -45,7 +45,7 @@ class Usecase: reference = ifcopenshell.api.run("document.add_reference", model, information=document) # Let's imagine storey represents an IfcBuildingStorey for the ground floor - ifcopenshell.api.run("document.assign_document", model, product=storey, document=reference) + ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference) # Now let's change our mind and remove the association ifcopenshell.api.run("document.unassign_document", model, product=storey, document=reference) diff --git a/src/ifcopenshell-python/test/api/document/test_assign_document.py b/src/ifcopenshell-python/test/api/document/test_assign_document.py index 6c76f844b4..32d584ac51 100644 --- a/src/ifcopenshell-python/test/api/document/test_assign_document.py +++ b/src/ifcopenshell-python/test/api/document/test_assign_document.py @@ -18,22 +18,25 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestAssignDocument(test.bootstrap.IFC4): def test_assigning_a_document(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) assert element.HasAssociations[0].RelatingDocument == reference + assert ifcopenshell.util.element.get_referenced_elements(reference) == {element} def test_assigning_multiple_documents(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) - ifcopenshell.api.run("document.assign_document", self.file, product=element2, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element, element2], document=reference) assert len(self.file.by_type("IfcRelAssociatesDocument")) == 1 - assert element.HasAssociations[0].RelatingDocument == reference - assert element2.HasAssociations[0].RelatingDocument == reference - assert element.HasAssociations[0] == element.HasAssociations[0] + assert ifcopenshell.util.element.get_referenced_elements(reference) == {element, element2} + + +class TestAssignDocumentIFC2X3(test.bootstrap.IFC2X3, TestAssignDocument): + pass diff --git a/src/ifcopenshell-python/test/api/document/test_remove_reference.py b/src/ifcopenshell-python/test/api/document/test_remove_reference.py index e5f5892172..a1b3c260e2 100644 --- a/src/ifcopenshell-python/test/api/document/test_remove_reference.py +++ b/src/ifcopenshell-python/test/api/document/test_remove_reference.py @@ -35,7 +35,7 @@ class TestRemoveReference(test.bootstrap.IFC4): wall = self.file.createIfcWall() information = ifcopenshell.api.run("document.add_information", self.file, parent=None) reference = ifcopenshell.api.run("document.add_reference", self.file, information=information) - ifcopenshell.api.run("document.assign_document", self.file, product=wall, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[wall], document=reference) assert len(self.file.by_type("IfcRelAssociatesDocument")) == 2 ifcopenshell.api.run("document.remove_reference", self.file, reference=reference) assert len(self.file.by_type("IfcDocumentReference")) == 0 diff --git a/src/ifcopenshell-python/test/api/document/test_unassign_document.py b/src/ifcopenshell-python/test/api/document/test_unassign_document.py index 8f227dd2b2..f58975ce69 100644 --- a/src/ifcopenshell-python/test/api/document/test_unassign_document.py +++ b/src/ifcopenshell-python/test/api/document/test_unassign_document.py @@ -24,7 +24,7 @@ class TestUnassignDocument(test.bootstrap.IFC4): def test_unassigning_a_document(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) assert not element.HasAssociations assert not len(self.file.by_type("IfcRelAssociatesDocument")) @@ -33,8 +33,8 @@ class TestUnassignDocument(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) - ifcopenshell.api.run("document.assign_document", self.file, product=element2, document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) + ifcopenshell.api.run("document.assign_document", self.file, products=[element2], document=reference) ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) assert not element.HasAssociations assert element2.HasAssociations[0].RelatingDocument == reference diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index a652a0de0c..66afb798a6 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -254,3 +254,10 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference) ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference) assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0 + + @deprecation_check + def test_assigning_a_document(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) + ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) + assert element.HasAssociations[0].RelatingDocument == reference diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 1a701f5ec9..97363f8017 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -787,7 +787,6 @@ class TestGetNestIFC2X3(test.bootstrap.IFC2X3, TestGetNestIFC4): class TestGetReferencedElements(test.bootstrap.IFC4): # TODO: test other references: - # IfcDocumentReference # IfcExternallyDefinedHatchStyle # IfcExternallyDefinedSurfaceStyle # IfcExternallyDefinedTextFont @@ -816,6 +815,15 @@ class TestGetReferencedElements(test.bootstrap.IFC4): ifcopenshell.api.run("library.assign_reference", self.file, reference=reference, products=elements) assert subject.get_referenced_elements(reference) == set(elements) + def test_get_elements_referenced_by_document_reference(self): + reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) + elements = [ + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"), + ] + ifcopenshell.api.run("document.assign_document", self.file, document=reference, products=elements) + assert subject.get_referenced_elements(reference) == set(elements) + class TestGetReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedElements): pass From 5da68721d64b52efd03891ba72164b8349676182 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 16:54:54 +0500 Subject: [PATCH 16/49] document.unassign_document - support batching #4474 --- src/blenderbim/blenderbim/core/document.py | 2 +- src/blenderbim/blenderbim/core/drawing.py | 2 +- src/blenderbim/test/core/test_document.py | 2 +- src/blenderbim/test/core/test_drawing.py | 2 +- .../ifcopenshell/api/__init__.py | 3 ++ .../api/document/unassign_document.py | 51 +++++++++++++------ .../api/document/test_unassign_document.py | 18 ++++--- src/ifcopenshell-python/test/api/test_api.py | 9 ++++ 8 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/blenderbim/blenderbim/core/document.py b/src/blenderbim/blenderbim/core/document.py index 85cfc00681..ce58e1edf4 100644 --- a/src/blenderbim/blenderbim/core/document.py +++ b/src/blenderbim/blenderbim/core/document.py @@ -113,4 +113,4 @@ def assign_document(ifc, product=None, document=None): def unassign_document(ifc, product=None, document=None): - ifc.run("document.unassign_document", product=product, document=document) + ifc.run("document.unassign_document", products=[product], document=document) diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index 127cbfa7db..cccdc16ad1 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -265,7 +265,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati ifc.run("group.assign_group", group=new_group, products=[new_annotation]) old_reference = drawing_tool.get_drawing_document(new_drawing) - ifc.run("document.unassign_document", product=new_drawing, document=old_reference) + ifc.run("document.unassign_document", products=[new_drawing], document=old_reference) information = ifc.run("document.add_information") uri = drawing_tool.get_default_drawing_path(drawing_name) diff --git a/src/blenderbim/test/core/test_document.py b/src/blenderbim/test/core/test_document.py index 2349870d39..9811c887a8 100644 --- a/src/blenderbim/test/core/test_document.py +++ b/src/blenderbim/test/core/test_document.py @@ -139,5 +139,5 @@ class TestAssignDocument: class TestUnassignDocument: def test_run(self, ifc): - ifc.run("document.unassign_document", product="product", document="document").should_be_called() + ifc.run("document.unassign_document", products=["product"], document="document").should_be_called() subject.unassign_document(ifc, product="product", document="document") diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index afe116d072..a2b6608f5d 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -391,7 +391,7 @@ class TestDuplicateDrawing: ifc.run("group.assign_group", group="new_group", products=["new_annotation"]).should_be_called() drawing.get_drawing_document("new_drawing").should_be_called().will_return("old_reference") - ifc.run("document.unassign_document", product="new_drawing", document="old_reference").should_be_called() + ifc.run("document.unassign_document", products=["new_drawing"], document="old_reference").should_be_called() ifc.run("document.add_information").should_be_called().will_return("information") ifc.run("document.add_reference", information="information").should_be_called().will_return("reference") diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index c82e076464..ff0a786292 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -102,6 +102,9 @@ ARGUMENTS_DEPRECATION = { "document.assign_document": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "document.unassign_document": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py index c4fbca8234..dd43573e65 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py @@ -17,16 +17,22 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None, document=None): - """Unassigns a document and a product association + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + document: ifcopenshell.entity_instance, + ): + """Unassigns a document and an association to the list of products - :param product: The object that the document reference or information is + :param product: The list of objects that the document reference or information is related to. - :type product: ifcopenshell.entity_instance.entity_instance + :type product: list[ifcopenshell.entity_instance.entity_instance] :param document: The IfcDocumentReference (typically) or in rare cases the IfcDocumentInformation that is associated with the product :type document: ifcopenshell.entity_instance.entity_instance @@ -48,21 +54,36 @@ class Usecase: ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference) # Now let's change our mind and remove the association - ifcopenshell.api.run("document.unassign_document", model, product=storey, document=reference) + ifcopenshell.api.run("document.unassign_document", model, products=[storey], document=reference) """ self.file = file self.settings = { - "product": product, + "products": products, "document": document, } def execute(self): - for rel in self.settings["product"].HasAssociations: - if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == self.settings["document"]: - if len(rel.RelatedObjects) == 1: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) - else: - rel.RelatedObjects = [o for o in rel.RelatedObjects if o != self.settings["product"]] + # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? + # NOTE: reuses code from `library.un assign_reference` + + reference_rels: set[ifcopenshell.entity_instance] = set() + products = set(self.settings["products"]) + for product in products: + reference_rels.update(product.HasAssociations) + + reference_rels = { + rel + for rel in reference_rels + if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == self.settings["document"] + } + + for rel in reference_rels: + related_objects = set(rel.RelatedObjects) - products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) diff --git a/src/ifcopenshell-python/test/api/document/test_unassign_document.py b/src/ifcopenshell-python/test/api/document/test_unassign_document.py index f58975ce69..bce6c8d6a1 100644 --- a/src/ifcopenshell-python/test/api/document/test_unassign_document.py +++ b/src/ifcopenshell-python/test/api/document/test_unassign_document.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestUnassignDocument(test.bootstrap.IFC4): @@ -25,16 +26,21 @@ class TestUnassignDocument(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) - ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) + ifcopenshell.api.run("document.unassign_document", self.file, products=[element], document=reference) assert not element.HasAssociations assert not len(self.file.by_type("IfcRelAssociatesDocument")) def test_unassigning_a_document_used_by_multiple_entities(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) - ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) - ifcopenshell.api.run("document.assign_document", self.file, products=[element2], document=reference) - ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) - assert not element.HasAssociations - assert element2.HasAssociations[0].RelatingDocument == reference + ifcopenshell.api.run( + "document.assign_document", self.file, products=[element, element2, element3], document=reference + ) + ifcopenshell.api.run("document.unassign_document", self.file, products=[element, element2], document=reference) + assert ifcopenshell.util.element.get_referenced_elements(reference) == {element3} + + +class TestUnassignDocumentIFC2X3(test.bootstrap.IFC2X3, TestUnassignDocument): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 66afb798a6..68fe0202c4 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -261,3 +261,12 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference) assert element.HasAssociations[0].RelatingDocument == reference + + @deprecation_check + def test_unassigning_a_document(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + reference = ifcopenshell.api.run("document.add_reference", self.file, information=None) + ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference) + ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) + assert not element.HasAssociations + assert not len(self.file.by_type("IfcRelAssociatesDocument")) From 9954e073fa0e994ee9a0823816fd91115d4f6a83 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 16:57:37 +0500 Subject: [PATCH 17/49] constraint.assign_constraint to handle owner history --- .../ifcopenshell/api/constraint/assign_constraint.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py index 1d77a7bb74..e680f129f5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api class Usecase: @@ -50,6 +51,7 @@ class Usecase: related_objects = set(rel.RelatedObjects) if rel.RelatedObjects else set() related_objects.add(self.settings["product"]) rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) return rel def get_constraint_rel(self): @@ -60,7 +62,7 @@ class Usecase: "IfcRelAssociatesConstraint", **{ "GlobalId": ifcopenshell.guid.new(), - # TODO: owner history + "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), "RelatingConstraint": self.settings["constraint"], } ) From ff76850a5489db82857a3f5d6664a0d4ddaddc71 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 17:12:16 +0500 Subject: [PATCH 18/49] ifcopenshell.util.element.get_structure_referenced_elements --- .../ifcopenshell/util/element.py | 21 +++++++++++++++++++ .../test/util/test_element.py | 16 ++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 5a97494737..f8b8791787 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -904,6 +904,27 @@ def get_referenced_structures(element: ifcopenshell.entity_instance) -> list[ifc return [r.RelatingStructure for r in getattr(element, "ReferencedInStructures", [])] +def get_structure_referenced_elements(structure: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + """Retreives a set of elements referenced by a structure + + :param structure: IfcSpatialElement + :type element: ifcopenshell.entity_instance.entity_instance + :return: A set of referenced elements, IfcSpatialReferenceSelect + :rtype: set[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + element = file.by_type("IfcBuildingStorey")[0] + print(ifcopenshell.util.element.get_structure_referenced_elements(element)) + """ + referenced = set() + for rel in structure.ReferencesElements: + referenced.update(rel.RelatedElements) + return referenced + + def get_decomposition(element: ifcopenshell.entity_instance, is_recursive=True) -> list[ifcopenshell.entity_instance]: """ Retrieves all subelements of an element based on the spatial decomposition diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 97363f8017..e90130e28e 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -726,6 +726,22 @@ class TestGetReferencedStructures(test.bootstrap.IFC4): assert subject.get_referenced_structures(element) == [building, building2] +class TestGetStructureReferencedElements(test.bootstrap.IFC4): + def test_getting_references_of_an_element(self): + building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + assert subject.get_structure_referenced_elements(building) == set() + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building) + assert subject.get_structure_referenced_elements(building) == {element} + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, product=element2, relating_structure=building) + assert subject.get_structure_referenced_elements(building) == {element, element2} + + +class TestGetStructureReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetStructureReferencedElements): + pass + + class TestGetDecompositionIFC4(test.bootstrap.IFC4): def test_getting_decomposed_subelements_of_an_element(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly") From 96b0a241d9530d5fbc9d4357e04f7acdff95ed5c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Apr 2024 17:16:09 +0500 Subject: [PATCH 19/49] add tests for get_referenced_structures --- src/ifcopenshell-python/test/util/test_element.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index e90130e28e..8efbeda360 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -726,6 +726,10 @@ class TestGetReferencedStructures(test.bootstrap.IFC4): assert subject.get_referenced_structures(element) == [building, building2] +class TestGetReferencedStructuresIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedStructures): + pass + + class TestGetStructureReferencedElements(test.bootstrap.IFC4): def test_getting_references_of_an_element(self): building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") From 4174a78bb57c83a6ce6a3ec68b47524051bd4cb8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 15 Apr 2024 23:30:39 +1000 Subject: [PATCH 20/49] #4553 --- src/blenderbim/blenderbim/tool/drawing.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index d8dcf606b8..d1d7e9649a 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -1775,7 +1775,6 @@ class Drawing(blenderbim.core.tool.Drawing): element_obj_names = set() for element in filtered_elements: obj = tool.Ifc.get_object(element) - element_obj_names.add(obj.name) current_representation = tool.Geometry.get_active_representation(obj) if current_representation: subcontext = current_representation.ContextOfItems @@ -1804,13 +1803,13 @@ class Drawing(blenderbim.core.tool.Drawing): # Don't hide IfcAnnotations as some of them might exist without representations if has_context or element.is_a("IfcAnnotation"): - # Note that render visibility is only set on drawing generation time for speed. - obj.hide_set(False) + element_obj_names.add(obj.name) + # Note that render visibility is only set on drawing generation time for speed. [ - obj.hide_set(False) + obj.hide_set(False) # Show the object for obj in bpy.context.view_layer.objects - if obj.name not in element_obj_names and tool.Ifc.get_entity(obj) + if obj.name in element_obj_names or not tool.Ifc.get_entity(obj) ] cls.import_camera_props(drawing, camera) From 5725ed867f3e44b19f39df4219dbf5b28961ff1b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 15 Apr 2024 23:31:38 +1000 Subject: [PATCH 21/49] Don't unnecessarily update NTS or scale pset data --- src/blenderbim/blenderbim/bim/module/drawing/prop.py | 10 ++++++++++ .../blenderbim/bim/module/search/operator.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index eedfefaee8..7773d41930 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -66,6 +66,11 @@ def get_location_hint(self, context): def update_diagram_scale(self, context): + if not context.scene.camera or context.scene.camera.data != self.id_data: + return + element = tool.Ifc.get_entity(context.scene.camera) + if not element: + return try: element = ( tool.Ifc.get() @@ -87,6 +92,11 @@ def update_diagram_scale(self, context): def update_is_nts(self, context): + if not context.scene.camera or context.scene.camera.data != self.id_data: + return + element = tool.Ifc.get_entity(context.scene.camera) + if not element: + return try: element = ( tool.Ifc.get() diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py index 9fee78e1ac..6ffe447470 100644 --- a/src/blenderbim/blenderbim/bim/module/search/operator.py +++ b/src/blenderbim/blenderbim/bim/module/search/operator.py @@ -235,7 +235,7 @@ class LoadSearch(Operator, tool.Ifc.Operator): def _execute(self, context): filter_groups = tool.Search.get_filter_groups(self.module) group = tool.Ifc.get().by_id(int(context.scene.BIMSearchProperties.saved_searches)) - query = tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) + tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) def draw(self, context): props = context.scene.BIMSearchProperties From 0983f9e66dc2362b9cc50ec7a38b6f531082bd73 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 15 Apr 2024 23:33:02 +1000 Subject: [PATCH 22/49] Fix bug where saved drawing filters didn't load in the UI. --- .../blenderbim/bim/module/drawing/operator.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 794877525b..1d5e751062 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -2570,8 +2570,16 @@ class EnableEditingElementFilter(bpy.types.Operator, Operator): def _execute(self, context): obj = bpy.context.scene.camera - if obj: - obj.data.BIMCameraProperties.filter_mode = self.filter_mode + if not obj: + return + obj.data.BIMCameraProperties.filter_mode = self.filter_mode + element = tool.Ifc.get_entity(obj) + if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()): + filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}") + try: + tool.Search.import_filter_query(query, filter_groups) + except: + pass class EditElementFilter(bpy.types.Operator, Operator): From ac64d0c400ba4743f16947bca979e691b3377b61 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 16 Apr 2024 08:06:10 +1000 Subject: [PATCH 23/49] Fix #3581. Invalid new document references. --- .../ifcopenshell/api/document/add_reference.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py index 39cdb109d6..bcec5da606 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py @@ -69,10 +69,12 @@ class Usecase: def execute(self) -> ifcopenshell.entity_instance: if self.file.schema == "IFC2X3": - reference = self.file.create_entity("IfcDocumentReference") + reference = self.file.create_entity("IfcDocumentReference", ItemReference="X") if self.settings["information"]: references = list(self.settings["information"].DocumentReferences or []) references.append(reference) self.settings["information"].DocumentReferences = references return reference - return self.file.create_entity("IfcDocumentReference", ReferencedDocument=self.settings["information"]) + return self.file.create_entity( + "IfcDocumentReference", ReferencedDocument=self.settings["information"], Identification="X" + ) From 23abf674f3ef4d0104787dcfbf5b61ce7c6891c6 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 16 Apr 2024 14:54:36 +1000 Subject: [PATCH 24/49] Minor fix --- src/ifcopenshell-python/ifcopenshell/util/shape.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index 4aaf6a08fe..798000005a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import shapely +import shapely.ops import numpy as np import ifcopenshell.util.element import ifcopenshell.util.placement From 1e41561fe88f21c81d336f22867baf03bb526f75 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 16 Apr 2024 16:43:02 +1000 Subject: [PATCH 25/49] Fix #4345. Removing grid axes are now handled specially. --- src/blenderbim/blenderbim/tool/geometry.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 7fb10700f2..0a3ff34fdf 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -91,6 +91,17 @@ class Geometry(blenderbim.core.tool.Geometry): if element.is_a("IfcRelSpaceBoundary"): ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element) return bpy.data.objects.remove(obj) + if element.is_a("IfcGridAxis"): + is_last_axis = False + # Deleting the last W axis is OK + if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or ( + (grid := element.PartOfV) and len(grid[0].VAxes) == 1 + ): + is_last_axis = True + if is_last_axis: + return + ifcopenshell.api.run("grid.remove_grid_axis", tool.Ifc.get(), axis=element) + return bpy.data.objects.remove(obj) collection = obj.BIMObjectProperties.collection if collection: From 3b6f79cc75f6f956b1a5998313cb1f19d853793c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 11:54:14 +0500 Subject: [PATCH 26/49] black format --- src/blenderbim/blenderbim/bim/module/constraint/operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/constraint/operator.py b/src/blenderbim/blenderbim/bim/module/constraint/operator.py index 6ab81a4695..2e9b9313da 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/operator.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/operator.py @@ -114,7 +114,7 @@ class EditObjective(bpy.types.Operator): ifcopenshell.api.run( "constraint.edit_objective", self.file, - **{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes} + **{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes}, ) bpy.ops.bim.load_objectives() return {"FINISHED"} @@ -191,7 +191,7 @@ class AssignConstraint(bpy.types.Operator): **{ "product": self.file.by_id(obj_id), "constraint": self.file.by_id(self.constraint), - } + }, ) return {"FINISHED"} From 8ac0d22a95009d52a69ab6e4874e327daf934b39 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 10:53:54 +0500 Subject: [PATCH 27/49] spatial.reference_structure - support batching #4474 --- src/blenderbim/blenderbim/core/spatial.py | 2 +- src/blenderbim/test/core/test_spatial.py | 2 +- .../ifcopenshell/api/__init__.py | 3 + .../api/spatial/dereference_structure.py | 4 +- .../api/spatial/reference_structure.py | 63 ++++++++++++------- .../api/spatial/test_dereference_structure.py | 8 +-- .../api/spatial/test_reference_structure.py | 34 +++++++--- src/ifcopenshell-python/test/api/test_api.py | 10 +++ .../test/util/test_element.py | 8 +-- 9 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index 57d627eaae..4532b86dba 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -32,7 +32,7 @@ def reference_structure( element: Optional[ifcopenshell.entity_instance] = None, ) -> Union[ifcopenshell.entity_instance, None]: if spatial.can_reference(structure, element): - return ifc.run("spatial.reference_structure", product=element, relating_structure=structure) + return ifc.run("spatial.reference_structure", products=[element], relating_structure=structure) def dereference_structure( diff --git a/src/blenderbim/test/core/test_spatial.py b/src/blenderbim/test/core/test_spatial.py index b77e95dc59..35282397fa 100644 --- a/src/blenderbim/test/core/test_spatial.py +++ b/src/blenderbim/test/core/test_spatial.py @@ -23,7 +23,7 @@ from test.core.bootstrap import ifc, collector, spatial class TestReferenceStructure: def test_run(self, ifc, spatial): spatial.can_reference("structure", "element").should_be_called().will_return(True) - ifc.run("spatial.reference_structure", product="element", relating_structure="structure").should_be_called() + ifc.run("spatial.reference_structure", products=["element"], relating_structure="structure").should_be_called() subject.reference_structure(ifc, spatial, structure="structure", element="element") diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index ff0a786292..0a77fe5da7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -105,6 +105,9 @@ ARGUMENTS_DEPRECATION = { "document.unassign_document": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "spatial.reference_structure": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py index 4a62532762..a9d4a1ad83 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py @@ -59,8 +59,8 @@ class Usecase: ifcopenshell.api.run("spatial.assign_container", model, products=[column], relating_structure=storey1) # And referenced in the others - ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey2) - ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey3) + ifcopenshell.api.run("spatial.reference_structure", model, products=[column], relating_structure=storey2) + ifcopenshell.api.run("spatial.reference_structure", model, products=[column], relating_structure=storey3) # Actually, it only goes up to storey 2. ifcopenshell.api.run("spatial.dereference_structure", model, product=column, relating_structure=storey3) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py index 187ce20afe..6d8915828a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py @@ -18,11 +18,18 @@ import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.element +from typing import Union class Usecase: - def __init__(self, file, product=None, relating_structure=None): - """Denote that a product is related to a spatial structure + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + relating_structure: ifcopenshell.entity_instance, + ): + """Denote that a list products is related to a list of spatial structures This is similar to ifcopenshell.api.spatial.assign_container, except that containment can only occur between a product and a single spatial @@ -39,13 +46,15 @@ class Usecase: Referencing is non-hierarchical, so a door may be referenced in multiple spaces simultaneously. - :param product: The physical IfcElement that exists in the space. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of physical IfcElements that exists in the space. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. + :type relating_structure: ifcopenshell.entity_instance.entity_instance :return: The IfcRelReferencedInSpatialStructure relationship instance - :rtype: ifcopenshell.entity_instance.entity_instance + or `None` if `products` was an empty list. + :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -73,37 +82,43 @@ class Usecase: ifcopenshell.api.run("spatial.assign_container", model, products=[column], relating_structure=storey1) # And referenced in the others - ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey2) - ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey3) + ifcopenshell.api.run( + "spatial.reference_structure", model, products=[column], relating_structure=[storey2, storey3] + ) """ self.file = file self.settings = { - "product": product, + "products": products, "relating_structure": relating_structure, } - def execute(self): - referenced_in_structures = self.settings["product"].ReferencedInStructures - references_elements = self.settings["relating_structure"].ReferencesElements + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + structure = self.settings["relating_structure"] + products = set(self.settings["products"]) - for rel in referenced_in_structures: - if rel.RelatingStructure == self.settings["relating_structure"]: - return + if not products: + return - if references_elements: - related_elements = list(references_elements[0].RelatedElements) - related_elements.append(self.settings["product"]) - references_elements[0].RelatedElements = related_elements - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": references_elements[0]}) - else: - references_elements = self.file.create_entity( + referenced = ifcopenshell.util.element.get_structure_referenced_elements(structure) + products_to_assign = products - referenced + rel = next(iter(structure.ReferencesElements), None) + + if not products_to_assign: + return rel + + if rel is None: + rel = self.file.create_entity( "IfcRelReferencedInSpatialStructure", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatedElements": [self.settings["product"]], - "RelatingStructure": self.settings["relating_structure"], + "RelatedElements": list(products_to_assign), + "RelatingStructure": structure, } ) + else: + related_elements = set(rel.RelatedElements) | products_to_assign + rel.RelatedElements = list(related_elements) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) - return references_elements + return rel diff --git a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py index 1cf2f3108e..d4d963222f 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py +++ b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py @@ -26,7 +26,7 @@ class TestDereferenceStructure(test.bootstrap.IFC4): def test_removing_a_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement], relating_structure=element) ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] @@ -40,14 +40,14 @@ class TestDereferenceStructure(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement1, relating_structure=element) - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement2, relating_structure=element) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement1], relating_structure=element) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement2], relating_structure=element) ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement1, relating_structure=element) assert self.file.by_type("IfcRelReferencedInSpatialStructure")[0].RelatedElements == (subelement2,) def test_deleting_the_rel_when_a_container_is_removed_with_no_elements(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement], relating_structure=element) ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) assert len(self.file.by_type("IfcRelReferencedInSpatialStructure")) == 0 diff --git a/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py index 21b6b0e8e8..0ef22d708a 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py +++ b/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py @@ -26,25 +26,39 @@ class TestReferenceStructure(test.bootstrap.IFC4): def test_referencing_a_structure(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - rel = ifcopenshell.api.run( - "spatial.reference_structure", self.file, product=subelement, relating_structure=element + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element ) - assert ifcopenshell.util.element.get_referenced_structures(subelement) == [element] - assert rel.is_a("IfcRelReferencedInSpatialStructure") + assert ifcopenshell.util.element.get_structure_referenced_elements(element) == {subelement, subelement2} def test_doing_nothing_if_the_structure_is_already_referenced(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) total_elements = len([e for e in self.file]) - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element) + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) assert len([e for e in self.file]) == total_elements def test_that_old_relationships_are_updated_if_they_still_contain_elements(self): - element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement1], relating_structure=element + ) subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement1, relating_structure=element1) - ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement2, relating_structure=element1) + subelement3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement2, subelement3], relating_structure=element + ) rel = subelement1.ReferencedInStructures[0] - assert len(rel.RelatedElements) == 2 + assert len(rel.RelatedElements) == 3 + + +class TestReferenceStructureIFC2X3(test.bootstrap.IFC2X3, TestReferenceStructure): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 68fe0202c4..e7cfeaf85f 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -270,3 +270,13 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference) assert not element.HasAssociations assert not len(self.file.by_type("IfcRelAssociatesDocument")) + + @deprecation_check + def test_referencing_a_structure(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + rel = ifcopenshell.api.run( + "spatial.reference_structure", self.file, product=subelement, relating_structure=element + ) + assert ifcopenshell.util.element.get_referenced_structures(subelement) == [element] + assert rel.is_a("IfcRelReferencedInSpatialStructure") diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 8efbeda360..ccb548a076 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -719,10 +719,10 @@ class TestGetReferencedStructures(test.bootstrap.IFC4): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") assert subject.get_referenced_structures(element) == [] building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building) assert subject.get_referenced_structures(element) == [building] building2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building2) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building2) assert subject.get_referenced_structures(element) == [building, building2] @@ -735,10 +735,10 @@ class TestGetStructureReferencedElements(test.bootstrap.IFC4): building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") assert subject.get_structure_referenced_elements(building) == set() element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building) assert subject.get_structure_referenced_elements(building) == {element} element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, product=element2, relating_structure=building) + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element2], relating_structure=building) assert subject.get_structure_referenced_elements(building) == {element, element2} From f1f8b3da6b120feaaf809b09f45cbdd52b7d556d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 10:59:50 +0500 Subject: [PATCH 28/49] spatial.dereference_structure - support batching #4474 --- src/blenderbim/blenderbim/core/spatial.py | 2 +- src/blenderbim/test/core/test_spatial.py | 2 +- .../ifcopenshell/api/__init__.py | 3 ++ .../api/spatial/dereference_structure.py | 30 ++++++++------ .../api/spatial/test_dereference_structure.py | 40 +++++++++++++------ src/ifcopenshell-python/test/api/test_api.py | 8 ++++ 6 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index 4532b86dba..171642900a 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -42,7 +42,7 @@ def dereference_structure( element: Optional[ifcopenshell.entity_instance] = None, ) -> None: if spatial.can_reference(structure, element): - return ifc.run("spatial.dereference_structure", product=element, relating_structure=structure) + return ifc.run("spatial.dereference_structure", products=[element], relating_structure=structure) def assign_container( diff --git a/src/blenderbim/test/core/test_spatial.py b/src/blenderbim/test/core/test_spatial.py index 35282397fa..9e01aefde4 100644 --- a/src/blenderbim/test/core/test_spatial.py +++ b/src/blenderbim/test/core/test_spatial.py @@ -30,7 +30,7 @@ class TestReferenceStructure: class TestDereferenceStructure: def test_run(self, ifc, spatial): spatial.can_reference("structure", "element").should_be_called().will_return(True) - ifc.run("spatial.dereference_structure", product="element", relating_structure="structure").should_be_called() + ifc.run("spatial.dereference_structure", products=["element"], relating_structure="structure").should_be_called() subject.dereference_structure(ifc, spatial, structure="structure", element="element") diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 0a77fe5da7..7bc04bc3ca 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -108,6 +108,9 @@ ARGUMENTS_DEPRECATION = { "spatial.reference_structure": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "spatial.dereference_structure": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py index a9d4a1ad83..00cb94d8b3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py @@ -22,11 +22,16 @@ import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None, relating_structure=None): - """Dereferences the a product and space + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + relating_structure: ifcopenshell.entity_instance, + ): + """Dereferences a list of products and space - :param product: The physical IfcElement that exists in the space. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of physical IfcElements that exists in the space. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. @@ -63,19 +68,20 @@ class Usecase: ifcopenshell.api.run("spatial.reference_structure", model, products=[column], relating_structure=storey3) # Actually, it only goes up to storey 2. - ifcopenshell.api.run("spatial.dereference_structure", model, product=column, relating_structure=storey3) + ifcopenshell.api.run("spatial.dereference_structure", model, products=[column], relating_structure=storey3) """ self.file = file - self.settings = {"product": product, "relating_structure": relating_structure} + self.settings = {"products": products, "relating_structure": relating_structure} - def execute(self): - for rel in self.settings["product"].ReferencedInStructures: - if rel.RelatingStructure != self.settings["relating_structure"]: + def execute(self) -> None: + products = set(self.settings["products"]) + for rel in self.settings["relating_structure"].ReferencesElements: + related_elements = set(rel.RelatedElements) + if not related_elements.intersection(products): continue - related_elements = list(rel.RelatedElements) - related_elements.remove(self.settings["product"]) + related_elements = related_elements - products if related_elements: - rel.RelatedElements = related_elements + rel.RelatedElements = list(related_elements) ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) else: history = rel.OwnerHistory diff --git a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py index d4d963222f..474f8bbcaf 100644 --- a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py +++ b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py @@ -26,28 +26,42 @@ class TestDereferenceStructure(test.bootstrap.IFC4): def test_removing_a_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement], relating_structure=element) - ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) + ifcopenshell.api.run( + "spatial.dereference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] + assert len(self.file.by_type("IfcRelReferencedInSpatialStructure")) == 0 def test_doing_nothing_if_no_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.dereference_structure", self.file, products=[subelement, subelement2], relating_structure=element + ) assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] + assert ifcopenshell.util.element.get_referenced_structures(subelement2) == [] def test_updating_the_rel_when_a_reference_is_removed_with_multipled_elements(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement1], relating_structure=element) - ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement2], relating_structure=element) - ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement1, relating_structure=element) - assert self.file.by_type("IfcRelReferencedInSpatialStructure")[0].RelatedElements == (subelement2,) + subelement3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement1], relating_structure=element + ) + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement2, subelement3], relating_structure=element + ) + ifcopenshell.api.run( + "spatial.dereference_structure", self.file, products=[subelement1, subelement2], relating_structure=element + ) + assert element.ReferencesElements[0].RelatedElements == (subelement3,) - def test_deleting_the_rel_when_a_container_is_removed_with_no_elements(self): - element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") - subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement], relating_structure=element) - ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) - assert len(self.file.by_type("IfcRelReferencedInSpatialStructure")) == 0 + +class TestDereferenceStructureIFC2X3(test.bootstrap.IFC2X3, TestDereferenceStructure): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index e7cfeaf85f..1615062cc2 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -280,3 +280,11 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): ) assert ifcopenshell.util.element.get_referenced_structures(subelement) == [element] assert rel.is_a("IfcRelReferencedInSpatialStructure") + + @deprecation_check + def test_removing_a_container(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") + subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement], relating_structure=element) + ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) + assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] From 02ae838c4520a7069fbb50cc9b095bbc30afaab9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 11:32:32 +0500 Subject: [PATCH 29/49] util.constraint - typing and docs --- .../ifcopenshell/util/constraint.py | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/constraint.py b/src/ifcopenshell-python/ifcopenshell/util/constraint.py index b2a38d6060..5df7f52846 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/util/constraint.py @@ -18,20 +18,43 @@ # # -def get_constraints(product): +import ifcopenshell +from typing import Union + + +def get_constraints(product: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: + """ + Retrieves the constraints assigned to the `product`. + + :param product: The IFC element. + :type product: ifcopenshell.entity_instance.entity_instance + :return: List of assigned constraints. + :rtype: list[ifcopenshell.entity_instance.entity_instance] + """ constraints = [] for rel in product.HasAssociations or []: if rel.is_a("IfcRelAssociatesConstraint"): constraints.append(rel.RelatingConstraint) return constraints -def get_metrics(constraint): + +def get_metrics(constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: + """ + Retrieves the list of nested constraints for a IfcObjective `constraint`. + + :param product: IfcObjective constraint. + :type product: ifcopenshell.entity_instance.entity_instance + :return: List of nested constraints. + :rtype: list[ifcopenshell.entity_instance.entity_instance] + """ + metrics = [] for metric in constraint.BenchmarkValues or []: metrics.append(metric) return metrics -def get_metric_reference(metric, is_deep=True): + +def get_metric_reference(metric: ifcopenshell.entity_instance, is_deep=True): def get_reference_Attribute(ref, path): if ref: if is_deep: @@ -47,7 +70,10 @@ def get_metric_reference(metric, is_deep=True): reference = metric.ReferencePath return get_reference_Attribute(reference, "") -def get_metric_constraints(resource, attribute): + +def get_metric_constraints( + resource: ifcopenshell.entity_instance, attribute +) -> Union[list[ifcopenshell.entity_instance], None]: metrics = [] for constraint in get_constraints(resource) or []: for metric in get_metrics(constraint) or []: @@ -60,15 +86,14 @@ def get_metric_constraints(resource, attribute): return metrics return None -def is_hard_constraint(metric): - if metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO": - return True -def is_attribute_locked(product, attribute): +def is_hard_constraint(metric: ifcopenshell.entity_instance) -> bool: + return metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO" + + +def is_attribute_locked(product: ifcopenshell.entity_instance, attribute) -> bool: is_locked = False - metrics = get_metric_constraints( - product, attribute - ) + metrics = get_metric_constraints(product, attribute) for metric in metrics or []: if is_hard_constraint(metric): is_locked = True From 81608b0ceebb3cc592706c41f905ee72eb4bc954 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 11:45:30 +0500 Subject: [PATCH 30/49] ifcopenshell.util.get_constrained_elements --- .../ifcopenshell/util/constraint.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/constraint.py b/src/ifcopenshell-python/ifcopenshell/util/constraint.py index 5df7f52846..b8f6aac14f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/util/constraint.py @@ -38,6 +38,22 @@ def get_constraints(product: ifcopenshell.entity_instance) -> list[ifcopenshell. return constraints +def get_constrained_elements(constraint: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: + """ + Retrieves the elements constrained by a `constraint`. + + :param product: The IFC element. + :type product: ifcopenshell.entity_instance.entity_instance + :return: Set of elements constrained by a `constrant`. + :rtype: set[ifcopenshell.entity_instance.entity_instance] + """ + elements = set() + for rel in constraint.file.get_inverse(constraint): + if rel.is_a("IfcRelAssociatesConstraint"): + elements.update(rel.RelatedObjects) + return elements + + def get_metrics(constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """ Retrieves the list of nested constraints for a IfcObjective `constraint`. From c808382154995ef6a0ee70214d5763dbb5803caf Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 11:24:45 +0500 Subject: [PATCH 31/49] constraint.assign_constraint - support batching #4474 --- .../bim/module/constraint/operator.py | 10 ++- src/blenderbim/blenderbim/core/resource.py | 2 +- .../ifcopenshell/api/__init__.py | 3 + .../api/constraint/assign_constraint.py | 61 +++++++++++++----- .../api/constraint/test_assign_constraint.py | 64 +++++++++++++++++++ src/ifcopenshell-python/test/api/test_api.py | 12 +++- 6 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py diff --git a/src/blenderbim/blenderbim/bim/module/constraint/operator.py b/src/blenderbim/blenderbim/bim/module/constraint/operator.py index 2e9b9313da..91315fedae 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/operator.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/operator.py @@ -179,17 +179,15 @@ class AssignConstraint(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - for obj in objs: - obj_id = obj.BIMObjectProperties.ifc_definition_id - if not obj_id: - continue + products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)] + if products: ifcopenshell.api.run( "constraint.assign_constraint", self.file, **{ - "product": self.file.by_id(obj_id), + "products": products, "constraint": self.file.by_id(self.constraint), }, ) diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index dd3875a04f..2d529c466a 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -195,7 +195,7 @@ def add_usage_constraint(ifc, resource_tool, resource=None, reference_path=None) }, ) ifc.run("constraint.add_metric_reference", metric=metric, reference_path=reference_path) - ifc.run("constraint.assign_constraint", product=resource, constraint=objective) + ifc.run("constraint.assign_constraint", products=[resource], constraint=objective) def remove_usage_constraint(ifc, resource_tool, resource, reference_path): diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 7bc04bc3ca..07eec78914 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -111,6 +111,9 @@ ARGUMENTS_DEPRECATION = { "spatial.dereference_structure": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "constraint.assign_constraint": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py index e680f129f5..759fab9a5e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py @@ -18,11 +18,17 @@ import ifcopenshell import ifcopenshell.api +from typing import Union class Usecase: - def __init__(self, file, product=None, constraint=None): - """Assigns a constraint to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + constraint: ifcopenshell.entity_instance, + ): + """Assigns a constraint to a list of products This assigns a relationship between a product and a constraint, so that when a product's properties and quantities do not match the requirements @@ -32,37 +38,58 @@ class Usecase: constraints are inherited from the type. This way, it is not necessary to create lots of constraint assignments. - :param product: The product the constraint applies to. This is anything + :param products: The list of products the constraint applies to. This is anything which can have properties or quantities. - :type product: ifcopenshell.entity_instance.entity_instance + :type products: list[ifcopenshell.entity_instance.entity_instance] :param constraint: The IfcObjective constraint :type constraint: ifcopenshell.entity_instance.entity_instance :return: The new or updated IfcRelAssociatesConstraint relationship + or `None` if `products` was an empty list. :rtype: ifcopenshell.entity_instance.entity_instance """ self.file = file self.settings = { - "product": product, + "products": products, "constraint": constraint, } - def execute(self): - rel = self.get_constraint_rel() - related_objects = set(rel.RelatedObjects) if rel.RelatedObjects else set() - related_objects.add(self.settings["product"]) - rel.RelatedObjects = list(related_objects) - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) - return rel + def execute(self) -> Union[ifcopenshell.entity_instance, None]: + products = set(self.settings["products"]) + if not products: + return + + self.constraint = self.settings["constraint"] + + rels = self.get_constraint_rels() + related_objects = set() + for rel in rels: + related_objects.update(rel.RelatedObjects) + + products_to_assign = products - related_objects + if not products_to_assign: + return rels[0] + + rel = next(iter(rels), None) + + if rel: + related_objects = set(rel.RelatedObjects) | products_to_assign + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + return rel - def get_constraint_rel(self): - for rel in self.file.by_type("IfcRelAssociatesConstraint"): - if rel.RelatingConstraint == self.settings["constraint"]: - return rel return self.file.create_entity( "IfcRelAssociatesConstraint", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file), - "RelatingConstraint": self.settings["constraint"], + "RelatingConstraint": self.constraint, + "RelatedObjects": list(products_to_assign), } ) + + def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]: + rels = [] + for rel in self.file.get_inverse(self.constraint): + if rel.is_a("IfcRelAssociatesConstraint"): + rels.append(rel) + return rels diff --git a/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py new file mode 100644 index 0000000000..f881ae9651 --- /dev/null +++ b/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py @@ -0,0 +1,64 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 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 pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.constraint + + +class TestAssignConstraint(test.bootstrap.IFC4): + def test_assign_a_constraint(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint + ) + assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element, element2} + assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 1 + + def test_doing_nothing_if_the_constraint_is_already_assigned(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint + ) + total_elements = len([e for e in self.file]) + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint + ) + assert len([e for e in self.file]) == total_elements + + def test_that_old_relationships_are_updated_if_they_still_contain_elements(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("constraint.assign_constraint", self.file, products=[element1], constraint=constraint) + rel = self.file.by_type("IfcRelAssociatesConstraint")[0] + + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element2, element3], constraint=constraint + ) + assert len(rel.RelatedObjects) == 3 + + +class TestAssignConstraintIFC2X3(test.bootstrap.IFC2X3, TestAssignConstraint): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 1615062cc2..5d99932ba8 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -19,6 +19,7 @@ import test.bootstrap import ifcopenshell.api import ifcopenshell.util.classification +import ifcopenshell.util.constraint import ifcopenshell.util.element import ifcopenshell.util.system from datetime import datetime @@ -285,6 +286,15 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): def test_removing_a_container(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding") subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") - ifcopenshell.api.run("spatial.reference_structure", self.file, products=[subelement], relating_structure=element) + ifcopenshell.api.run( + "spatial.reference_structure", self.file, products=[subelement], relating_structure=element + ) ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element) assert ifcopenshell.util.element.get_referenced_structures(subelement) == [] + + @deprecation_check + def test_assign_a_constraint(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + ifcopenshell.api.run("constraint.assign_constraint", self.file, product=element, constraint=constraint) + assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element} From 9a9be821d167ab7d42ddc34f9934a0922c7061a0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 11:52:42 +0500 Subject: [PATCH 32/49] constraint.unassign_constraint - support batching #4474 --- .../bim/module/constraint/operator.py | 12 ++-- src/blenderbim/blenderbim/core/resource.py | 2 +- .../ifcopenshell/api/__init__.py | 3 + .../api/constraint/unassign_constraint.py | 56 +++++++++++++--- .../constraint/test_unassign_constraint.py | 67 +++++++++++++++++++ src/ifcopenshell-python/test/api/test_api.py | 13 ++++ 6 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py diff --git a/src/blenderbim/blenderbim/bim/module/constraint/operator.py b/src/blenderbim/blenderbim/bim/module/constraint/operator.py index 91315fedae..f461554c49 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/operator.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/operator.py @@ -205,18 +205,16 @@ class UnassignConstraint(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - for obj in objs: - obj_id = obj.BIMObjectProperties.ifc_definition_id - if not obj_id: - continue + products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)] + if products: ifcopenshell.api.run( "constraint.unassign_constraint", self.file, **{ - "product": self.file.by_id(obj_id), + "products": products, "constraint": self.file.by_id(self.constraint), - } + }, ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py index 2d529c466a..63524cd03c 100644 --- a/src/blenderbim/blenderbim/core/resource.py +++ b/src/blenderbim/blenderbim/core/resource.py @@ -206,7 +206,7 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path): reference = resource_tool.get_metric_reference(metric, is_deep=True) if reference == reference_path: ifc.run("constraint.remove_metric", metric=metric) - ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint) + ifc.run("constraint.unassign_constraint", products=[resource], constraint=constraint) ifc.run("constraint.remove_constraint", constraint=constraint) diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 07eec78914..f5a56279c0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -114,6 +114,9 @@ ARGUMENTS_DEPRECATION = { "constraint.assign_constraint": partial( batching_argument_deprecation, prev_argument="product", new_argument="products" ), + "constraint.unassign_constraint": partial( + batching_argument_deprecation, prev_argument="product", new_argument="products" + ), } diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py index 66b3bc94b9..dbc1e1b7fb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py @@ -17,18 +17,24 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api import ifcopenshell.util.element class Usecase: - def __init__(self, file, product=None, constraint=None): - """Unassigns a constraint to a product + def __init__( + self, + file: ifcopenshell.file, + products: list[ifcopenshell.entity_instance], + constraint: ifcopenshell.entity_instance, + ): + """Unassigns a constraint from a list of products The constraint will not be deleted and is available to be assigned to other products. - :param product: The product the constraint applies to. - :type product: ifcopenshell.entity_instance.entity_instance + :param products: The list of products the constraint applies to. + :type products: list[ifcopenshell.entity_instance.entity_instance] :param constraint: The IfcObjective constraint :type constraint: ifcopenshell.entity_instance.entity_instance :return: None @@ -36,14 +42,42 @@ class Usecase: """ self.file = file self.settings = { - "product": product, + "products": products, "constraint": constraint, } def execute(self): - for rel in self.settings["product"].HasAssociations: - if rel.is_a("IfcRelAssociatesConstraint") and rel.RelatingConstraint == self.settings["constraint"]: - history = rel.OwnerHistory - self.file.remove(rel) - if history: - ifcopenshell.util.element.remove_deep2(self.file, history) + products = set(self.settings["products"]) + if not products: + return + + self.constraint = self.settings["constraint"] + rels = self.get_constraint_rels() + related_objects = set() + for rel in rels: + related_objects.update(rel.RelatedObjects) + + if not related_objects.intersection(products): + return + + for rel in rels: + related_objects = set(rel.RelatedObjects) + if not related_objects.intersection(products): + continue + related_objects -= products + if related_objects: + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + continue + + history = rel.OwnerHistory + self.file.remove(rel) + if history: + ifcopenshell.util.element.remove_deep2(self.file, history) + + def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]: + rels = [] + for rel in self.file.get_inverse(self.constraint): + if rel.is_a("IfcRelAssociatesConstraint"): + rels.append(rel) + return rels diff --git a/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py new file mode 100644 index 0000000000..0d8a21b8bf --- /dev/null +++ b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py @@ -0,0 +1,67 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 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 pytest +import test.bootstrap +import ifcopenshell.api +import ifcopenshell.util.constraint + + +class TestUnassignConstraint(test.bootstrap.IFC4): + def test_unassigning_a_constraint(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint + ) + ifcopenshell.api.run( + "constraint.unassign_constraint", self.file, products=[element, element2], constraint=constraint + ) + assert ifcopenshell.util.constraint.get_constrained_elements(element) == set() + assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0 + + def test_doing_nothing_if_no_constraint(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.unassign_constraint", self.file, products=[element, element2], constraint=constraint + ) + assert ifcopenshell.util.constraint.get_constrained_elements(element) == set() + assert ifcopenshell.util.constraint.get_constrained_elements(element2) == set() + + def test_updating_the_rel_when_a_reference_is_removed_with_multipled_elements(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run("constraint.assign_constraint", self.file, products=[element1], constraint=constraint) + rel = self.file.by_type("IfcRelAssociatesConstraint")[0] + + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, products=[element2, element3], constraint=constraint + ) + ifcopenshell.api.run( + "constraint.unassign_constraint", self.file, products=[element1, element2], constraint=constraint + ) + assert rel.RelatedObjects == (element3,) + + +class TestUnassignConstraintIFC2X3(test.bootstrap.IFC2X3, TestUnassignConstraint): + pass diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py index 5d99932ba8..72d20392f3 100644 --- a/src/ifcopenshell-python/test/api/test_api.py +++ b/src/ifcopenshell-python/test/api/test_api.py @@ -298,3 +298,16 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4): constraint = ifcopenshell.api.run("constraint.add_objective", self.file) ifcopenshell.api.run("constraint.assign_constraint", self.file, product=element, constraint=constraint) assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element} + + @deprecation_check + def test_unassigning_a_constraint(self): + constraint = ifcopenshell.api.run("constraint.add_objective", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + ifcopenshell.api.run( + "constraint.assign_constraint", self.file, product=element, constraint=constraint + ) + ifcopenshell.api.run( + "constraint.unassign_constraint", self.file, product=element, constraint=constraint + ) + assert ifcopenshell.util.constraint.get_constrained_elements(element) == set() + assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0 From aeea9dfb5eddcb767b8baad0c0c2e9f8a64c2ae0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 12:12:23 +0500 Subject: [PATCH 33/49] fix test after ac64d0c40 --- src/blenderbim/test/tool/test_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/test/tool/test_document.py b/src/blenderbim/test/tool/test_document.py index 3ef56744e0..b458eedf1e 100644 --- a/src/blenderbim/test/tool/test_document.py +++ b/src/blenderbim/test/tool/test_document.py @@ -188,7 +188,7 @@ class TestImportReferences(NewFile): assert len(props.documents) == 1 assert props.documents[0].ifc_definition_id == reference.id() assert props.documents[0].name == "Unnamed" - assert props.documents[0].identification == "*" + assert props.documents[0].identification == "X" assert props.documents[0].is_information is False From e1e169ece80098202fbf0c25d94750a0f3da1692 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 12:25:01 +0500 Subject: [PATCH 34/49] fix bug in d6e69bd6e --- src/blenderbim/blenderbim/bim/module/classification/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py index 74c84eae21..623aa59770 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/operator.py +++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py @@ -294,7 +294,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator): active_reference = tool.Ifc.get().by_id(self.reference) identification = active_reference[1] - elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = [] + elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {} for obj in objects: ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) From b458aea6b971078432d1145f2784945436b157e3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 14:21:21 +0500 Subject: [PATCH 35/49] ifctester docs for command line use --- src/ifctester/README.md | 16 ++++++++++++++++ src/ifctester/ifctester/__main__.py | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ifctester/README.md b/src/ifctester/README.md index 5b3487675e..976d0d2482 100644 --- a/src/ifctester/README.md +++ b/src/ifctester/README.md @@ -4,6 +4,22 @@ With **IfcTester**, you can author and read **Information Delivery Specification ## How to use it +### Command line use + +.. code-block:: bash + + # run console reporter + python -m ifctester example.ids example.ifc + python -m ifctester example.ids example.ifc -r Html -o report.html + +Available flags: + +- ``-r`` / ``--reporter``: The reporting method to view audit results. Availabe reporters: Console, Txt, Json, Html, Ods, Bcf +- ``--no-color``: Disable colour output (supported by Console reporting). +- ``-o`` / ``--output``: Output file (supported for all types of reporting except Console). + +### Code example + ```python import ifcopenshell from ifctester import ids, reporter diff --git a/src/ifctester/ifctester/__main__.py b/src/ifctester/ifctester/__main__.py index c5c70cd9c2..05b2c6ec29 100644 --- a/src/ifctester/ifctester/__main__.py +++ b/src/ifctester/ifctester/__main__.py @@ -31,10 +31,10 @@ parser.add_argument( "-r", "--reporter", type=str, help="The reporting method to view audit results", default="Console" ) parser.add_argument( - "--no-color", help="Disable colour output supported by Console reporting", action="store_true" + "--no-color", help="Disable colour output (supported by Console reporting)", action="store_true" ) parser.add_argument( - "-o", "--output", help="Output file supported by Json reporting" + "-o", "--output", help="Output file (supported for all types of reporting except Console)" ) args = parser.parse_args() From 836293504cab9eed5e8eaa0996a5922f777ed9d7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 14:21:46 +0500 Subject: [PATCH 36/49] ifctester typing --- src/blenderbim/test/tool/test_document.py | 1 + src/ifctester/ifctester/facet.py | 11 +- src/ifctester/ifctester/ids.py | 14 +- src/ifctester/ifctester/reporter.py | 200 +++++++++++++++------- 4 files changed, 157 insertions(+), 69 deletions(-) diff --git a/src/blenderbim/test/tool/test_document.py b/src/blenderbim/test/tool/test_document.py index b458eedf1e..8b22ce19d0 100644 --- a/src/blenderbim/test/tool/test_document.py +++ b/src/blenderbim/test/tool/test_document.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell +import ifcopenshell.api import blenderbim.core.tool import blenderbim.tool as tool from test.bim.bootstrap import NewFile diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 2048d1b307..867e06b36c 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -24,7 +24,7 @@ import ifcopenshell.util.element import ifcopenshell.util.classification from functools import lru_cache from xmlschema.validators import identities -from typing import Union, Optional, Any, Literal, TYPE_CHECKING +from typing import Union, Optional, Any, Literal, TYPE_CHECKING, TypedDict from logging import Logger if TYPE_CHECKING: @@ -61,12 +61,17 @@ def get_psets(element): Cardinality = Literal["required", "optional", "prohibited"] +class FacetFailure(TypedDict): + element: ifcopenshell.entity_instance + reason: str + + class Facet: cardinality: Cardinality def __init__(self, *parameters): self.status = None - self.failures = [] + self.failures: list[FacetFailure] = [] for i, name in enumerate(self.parameters): setattr(self, name.replace("@", ""), parameters[i]) @@ -105,7 +110,7 @@ class Facet: clause_type: str, specification: Optional[Specification] = None, requirement: Optional[Facet] = None, - ): + ) -> str: if clause_type == "applicability": templates = self.applicability_templates elif clause_type == "requirement": diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py index da71c87c96..4703b0445a 100644 --- a/src/ifctester/ifctester/ids.py +++ b/src/ifctester/ifctester/ids.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcTester. If not, see . +from __future__ import annotations import os import datetime import ifcopenshell @@ -34,14 +35,19 @@ from .facet import ( get_pset, get_psets, Cardinality, + FacetFailure, ) -from typing import List, Optional, Union +from typing import List, Optional, Union, overload, Literal cwd = os.path.dirname(os.path.realpath(__file__)) schema = None -def open(filepath, validate=False): +@overload +def open(filepath: str, validate: Literal[False] = False) -> Ids: ... +@overload +def open(filepath: str, validate: Literal[True]) -> None: ... +def open(filepath: str, validate=False) -> Union[Ids, None]: if validate: get_schema().validate(filepath) return Ids().parse( @@ -265,11 +271,11 @@ class Specification: if self.maxOccurs != 0: # This is a required or optional specification if not is_pass: self.failed_entities.add(element) - facet.failures.append({"element": element, "reason": str(result)}) + facet.failures.append(FacetFailure(element=element, reason=str(result))) else: # This is a prohibited specification if is_pass: self.failed_entities.add(element) - facet.failures.append({"element": element, "reason": str(result)}) + facet.failures.append(FacetFailure(element=element, reason=str(result))) self.status = True for facet in self.requirements: diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index ea77e6a35f..c40979abb5 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcTester. If not, see . +from __future__ import annotations import os import sys import math @@ -24,12 +25,15 @@ import datetime import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.element +from .ids import Specification, Ids +from .facet import Facet, FacetFailure +from typing import TypedDict, Union, Literal, Optional cwd = os.path.dirname(os.path.realpath(__file__)) class Reporter: - def __init__(self, ids): + def __init__(self, ids: Ids): self.ids = ids def report(self, ids): @@ -42,8 +46,79 @@ class Reporter: pass +ResultsPercent = Union[int, Literal["N/A"]] + + +class Results(TypedDict): + title: str + date: str + filepath: str + filename: str + specifications: list[ResultsSpecification] + status: bool + total_specifications: int + total_specifications: int + total_specifications_pass: int + total_specifications_fail: int + percent_specifications_pass: ResultsPercent + total_requirements: int + total_requirements_pass: int + total_requirements_fail: int + percent_requirements_pass: ResultsPercent + total_checks: int + total_checks_pass: int + total_checks_fail: int + percent_checks_pass: ResultsPercent + + +class ResultsSpecification(TypedDict): + name: str + description: str + instructions: str + status: bool + total_applicable: int + total_applicable_pass: int + total_applicable_fail: int + percent_applicable_pass: ResultsPercent + total_checks: int + total_checks_pass: int + total_checks_fail: int + percent_checks_pass: ResultsPercent + required: bool + applicability: list[str] + requirements: list[ResultsRequirement] + + +class ResultsRequirement(TypedDict): + description: str + status: bool + failed_entities: list[ResultsFailedEntity] + total_applicable: int + total_pass: int + total_fail: int + percent_pass: ResultsPercent + + +# use different syntax because of the "class" key +ResultsFailedEntity = TypedDict( + "ResultsFailedEntity", + { + "reason": str, + "element": str, + "element_type": str, + "class": str, + "predefined_type": str, + "name": Union[str, None], + "description": Union[str, None], + "id": int, + "global_id": Union[str, None], + "tag": Union[str, None], + }, +) + + class Console(Reporter): - def __init__(self, ids, use_colour=True): + def __init__(self, ids: Ids, use_colour=True): super().__init__(ids) self.use_colour = use_colour self.colours = { @@ -59,14 +134,14 @@ class Console(Reporter): "reverse": "\033[;7m", } - def report(self): + def report(self) -> None: self.set_style("bold", "blue") self.print(self.ids.info.get("title", "Untitled IDS")) for specification in self.ids.specifications: self.report_specification(specification) self.set_style("reset") - def report_specification(self, specification): + def report_specification(self, specification: Specification) -> None: if specification.status is True: self.set_style("bold", "green") self.print("[PASS] ", end="") @@ -113,7 +188,7 @@ class Console(Reporter): self.print(" " * 12 + f"... {len(requirement.failures)} in total ...") self.set_style("reset") - def report_reason(self, failure): + def report_reason(self, failure: FacetFailure) -> None: is_bold = False for substring in failure["reason"].split('"'): if is_bold: @@ -126,11 +201,11 @@ class Console(Reporter): self.print(" - " + str(failure["element"])) self.set_style("reset") - def set_style(self, *colours): + def set_style(self, *colours: str): if self.use_colour: sys.stdout.write("".join([self.colours[c] for c in colours])) - def print(self, txt, end=None): + def print(self, txt: str, end: Optional[str] = None): if end is not None: print(txt, end=end) else: @@ -138,14 +213,14 @@ class Console(Reporter): class Txt(Console): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids, use_colour=False) self.text = "" - def print(self, txt, end=None): + def print(self, txt: str, end: Optional[str] = None): self.text += txt + "\n" if end is None else txt - def to_string(self): + def to_string(self) -> None: print(self.text) def to_file(self, filepath: str) -> None: @@ -154,11 +229,11 @@ class Txt(Console): class Json(Reporter): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids) - self.results = {} + self.results = Results() - def report(self): + def report(self) -> Results: self.results["title"] = self.ids.info.get("title", "Untitled IDS") self.results["date"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.results["filepath"] = self.ids.filepath @@ -203,7 +278,7 @@ class Json(Reporter): ) return self.results - def report_specification(self, specification): + def report_specification(self, specification: Specification) -> ResultsSpecification: applicability = [a.to_string("applicability") for a in specification.applicability] total_applicable = len(specification.applicable_entities) total_checks = 0 @@ -216,57 +291,60 @@ class Json(Reporter): total_checks += total_applicable total_checks_pass += total_pass requirements.append( - { - "description": requirement.to_string("requirement", specification, requirement), - "status": requirement.status, - "failed_entities": self.report_failed_entities(requirement), - "total_applicable": total_applicable, - "total_pass": total_pass, - "total_fail": total_fail, - "percent_pass": percent_pass, - } + ResultsRequirement( + description=requirement.to_string("requirement", specification, requirement), + status=requirement.status, + failed_entities=self.report_failed_entities(requirement), + total_applicable=total_applicable, + total_pass=total_pass, + total_fail=total_fail, + percent_pass=percent_pass, + ) ) total_applicable_pass = total_applicable - len(specification.failed_entities) percent_applicable_pass = ( math.floor((total_applicable_pass / total_applicable) * 100) if total_applicable else "N/A" ) percent_checks_pass = math.floor((total_checks_pass / total_checks) * 100) if total_checks else "N/A" - return { - "name": specification.name, - "description": specification.description, - "instructions": specification.instructions, - "status": specification.status, - "total_applicable": total_applicable, - "total_applicable_pass": total_applicable_pass, - "total_applicable_fail": total_applicable - total_applicable_pass, - "percent_applicable_pass": percent_applicable_pass, - "total_checks": total_checks, - "total_checks_pass": total_checks_pass, - "total_checks_fail": total_checks - total_checks_pass, - "percent_checks_pass": percent_checks_pass, - "required": specification.minOccurs != 0, - "applicability": applicability, - "requirements": requirements, - } - def report_failed_entities(self, requirement): + return ResultsSpecification( + name=specification.name, + description=specification.description, + instructions=specification.instructions, + status=specification.status, + total_applicable=total_applicable, + total_applicable_pass=total_applicable_pass, + total_applicable_fail=total_applicable - total_applicable_pass, + percent_applicable_pass=percent_applicable_pass, + total_checks=total_checks, + total_checks_pass=total_checks_pass, + total_checks_fail=total_checks - total_checks_pass, + percent_checks_pass=percent_checks_pass, + required=specification.minOccurs != 0, + applicability=applicability, + requirements=requirements, + ) + + def report_failed_entities(self, requirement: Facet) -> list[ResultsFailedEntity]: return [ - { - "reason": f["reason"], - "element": str(f["element"]), - "element_type": str(ifcopenshell.util.element.get_type(f["element"])), - "class": f["element"].is_a(), - "predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]), - "name": getattr(f["element"], "Name", None), - "description": getattr(f["element"], "Description", None), - "id": f["element"].id(), - "global_id": getattr(f["element"], "GlobalId", None), - "tag": getattr(f["element"], "Tag", None), - } + ResultsFailedEntity( + { + "reason": f["reason"], + "element": str(f["element"]), + "element_type": str(ifcopenshell.util.element.get_type(f["element"])), + "class": f["element"].is_a(), + "predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]), + "name": getattr(f["element"], "Name", None), + "description": getattr(f["element"], "Description", None), + "id": f["element"].id(), + "global_id": getattr(f["element"], "GlobalId", None), + "tag": getattr(f["element"], "Tag", None), + } + ) for f in requirement.failures ] - def to_string(self): + def to_string(self) -> str: import json return json.dumps(self.results) @@ -279,11 +357,10 @@ class Json(Reporter): class Html(Json): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids) - self.results = {} - def report(self): + def report(self) -> None: super().report() entity_limit = 100 for spec in self.results["specifications"]: @@ -294,7 +371,7 @@ class Html(Json): requirement["total_entities"] = total requirement["total_omitted"] = total - entity_limit - def to_string(self): + def to_string(self) -> str: import pystache with open(os.path.join(cwd, "templates", "report.html"), "r") as file: @@ -309,7 +386,7 @@ class Html(Json): class Ods(Json): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids) self.colours = { "h": "cccccc", # Header @@ -317,7 +394,6 @@ class Ods(Json): "f": "fb5a3e", # Fail "t": "ffffff", # Regular text } - self.results = {} def to_file(self, filepath: str) -> None: from odf.opendocument import OpenDocumentSpreadsheet @@ -423,8 +499,8 @@ class Ods(Json): class Bcf(Json): - def report_failed_entities(self, requirement): - return [{"reason": f["reason"], "element": f["element"]} for f in requirement.failures] + def report_failed_entities(self, requirement: Facet) -> list[FacetFailure]: + return [FacetFailure(f) for f in requirement.failures] def to_file(self, filepath: str) -> None: import numpy as np From 708ee7350bc1a9a211e9c5c4ce08182edf9264b8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 14:23:37 +0500 Subject: [PATCH 37/49] ifctester - fix bug adding second .ods extension --- src/ifctester/ifctester/reporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index c40979abb5..47d50c7a07 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -495,7 +495,7 @@ class Ods(Json): table.addElement(tr) self.doc.spreadsheet.addElement(table) - self.doc.save(filepath, addsuffix=filepath.lower().endswith(".ods")) + self.doc.save(filepath, addsuffix=not filepath.lower().endswith(".ods")) class Bcf(Json): From 6ee75029201a234fa541bf5bd74a09edf15493a2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 16:16:44 +0500 Subject: [PATCH 38/49] ifctester.reporter.Txt small fix --- src/ifctester/ifctester/reporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index 47d50c7a07..d83dc3d080 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -218,7 +218,7 @@ class Txt(Console): self.text = "" def print(self, txt: str, end: Optional[str] = None): - self.text += txt + "\n" if end is None else txt + self.text += txt + "\n" if end is None else end def to_string(self) -> None: print(self.text) From 89d29cf1336c4f1d216a2cb51041aab69dfc4c29 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 16:40:05 +0500 Subject: [PATCH 39/49] ifctester.reporter.Ods - option for safe excel export #4427 The issue was related to the sheet names - Excel prohibits some symbols in the names and it doesn't allow sheet name to be longer than 31 symbols. If sheet names doesn't match this criteria then Excel says file appears to be corrupted - when file open data itself is not really corrupted besides the spreadsheet names that failed the criteria. --- src/ifctester/README.md | 1 + src/ifctester/ifctester/__main__.py | 5 ++++- src/ifctester/ifctester/reporter.py | 35 ++++++++++++++++++++++++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/ifctester/README.md b/src/ifctester/README.md index 976d0d2482..f145f00804 100644 --- a/src/ifctester/README.md +++ b/src/ifctester/README.md @@ -16,6 +16,7 @@ Available flags: - ``-r`` / ``--reporter``: The reporting method to view audit results. Availabe reporters: Console, Txt, Json, Html, Ods, Bcf - ``--no-color``: Disable colour output (supported by Console reporting). +- ``--excel-safe``: Make sure exported ODS is safely exported for Excel. - ``-o`` / ``--output``: Output file (supported for all types of reporting except Console). ### Code example diff --git a/src/ifctester/ifctester/__main__.py b/src/ifctester/ifctester/__main__.py index 05b2c6ec29..a49c4c1094 100644 --- a/src/ifctester/ifctester/__main__.py +++ b/src/ifctester/ifctester/__main__.py @@ -33,6 +33,9 @@ parser.add_argument( parser.add_argument( "--no-color", help="Disable colour output (supported by Console reporting)", action="store_true" ) +parser.add_argument( + "--excel-safe", help="Make sure exported ODS is safely exported for Excel", action="store_true" +) parser.add_argument( "-o", "--output", help="Output file (supported for all types of reporting except Console)" ) @@ -56,7 +59,7 @@ elif args.reporter == "Json": elif args.reporter == "Html": engine = reporter.Html(specs) elif args.reporter == "Ods": - engine = reporter.Ods(specs) + engine = reporter.Ods(specs, excel_safe=args.excel_safe) elif args.reporter == "Bcf": engine = reporter.Bcf(specs) diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index d83dc3d080..90f7fd1c36 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.py @@ -18,6 +18,7 @@ from __future__ import annotations import os +import re import sys import math import logging @@ -386,8 +387,9 @@ class Html(Json): class Ods(Json): - def __init__(self, ids: Ids): + def __init__(self, ids: Ids, excel_safe=False): super().__init__(ids) + self.excel_safe = excel_safe self.colours = { "h": "cccccc", # Header "p": "97cc64", # Pass @@ -395,6 +397,33 @@ class Ods(Json): "t": "ffffff", # Regular text } + def excel_safe_spreadsheet_name(self, name: str) -> str: + if not self.excel_safe: + return name + + warning = ( + f'WARNING. Sheet name "{name}" is not valid for Excel and will be changed. ' + "See: https://support.microsoft.com/en-us/office/rename-a-worksheet-3f1f7148-ee83-404d-8ef0-9ff99fbad1f9" + ) + + if not name or name == "History": + print(warning) + return "placeholder spreadsheet name" + + if name.startswith("'") or name.endswith("'"): + print(warning) + name = name.strip("'") + + pattern = r"[\\\/\?\*\:\[\]]" + if re.search(pattern, name): + name = re.sub(pattern, "", name) + print(warning) + + if len(name) > 31: + name = name[:31] + print(warning) + return name + def to_file(self, filepath: str) -> None: from odf.opendocument import OpenDocumentSpreadsheet from odf.style import Style, TableCellProperties @@ -410,7 +439,7 @@ class Ods(Json): self.doc.automaticstyles.addElement(style) self.cell_formats[key] = style - table = Table(name=self.results["title"]) + table = Table(name=self.excel_safe_spreadsheet_name(self.results["title"])) tr = TableRow() for header in ["Specification", "Status", "Total Pass", "Total Checks", "Percentage Pass"]: tc = TableCell(valuetype="string", stylename="h") @@ -447,7 +476,7 @@ class Ods(Json): for specification in self.results["specifications"]: if specification["status"]: continue - table = Table(name=specification["name"]) + table = Table(name=self.excel_safe_spreadsheet_name(specification["name"])) tr = TableRow() for header in [ "Requirement", From 0a542e695702a1844059ff9169fc853b67aaae67 Mon Sep 17 00:00:00 2001 From: Andrej Date: Wed, 17 Apr 2024 11:38:18 +0500 Subject: [PATCH 40/49] ci-ifcsverchok-build.yml --- .github/workflows/ci-ifcsverchok-build.yml | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/ci-ifcsverchok-build.yml diff --git a/.github/workflows/ci-ifcsverchok-build.yml b/.github/workflows/ci-ifcsverchok-build.yml new file mode 100644 index 0000000000..637adc4441 --- /dev/null +++ b/.github/workflows/ci-ifcsverchok-build.yml @@ -0,0 +1,53 @@ +name: Publish-ifcsverchok + +on: + push: + paths: + - '.github/workflows/ci-ifcsverchok-build.yml' + - 'src/ifcsverchok/*' + branches: + - v0.7.0 + +env: + major: 0 + minor: 0 + name: ifcsverchok + +jobs: + activate: + runs-on: ubuntu-latest + if: | + github.repository == 'IfcOpenShell/IfcOpenShell' + steps: + - name: Set env + run: echo ok go + + build: + needs: activate + name: ifcsverchok + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 # https://github.com/actions/setup-python + with: + architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified + python-version: '3.11' + - run: echo ${{ env.DATE }} + - name: Get current date + id: date + run: echo "::set-output name=date::$(date +'%y%m%d')" + - name: Compile + run: | + cd src/ifcsverchok + make dist + - name: Upload Zip file to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: src/ifcsverchok/dist/ifcsverchok-${{steps.date.outputs.date}}.zip + asset_name: ifcsverchok-${{steps.date.outputs.date}}.zip + tag: "ifcsverchok-${{steps.date.outputs.date}}" + overwrite: true + body: "ifcsverchok build for ${{steps.date.outputs.date}}" From 31cd20c61b988cf1a1c381e081aefa9fd395b4ad Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Apr 2024 12:08:14 +0500 Subject: [PATCH 41/49] more user-friendly errors enabling ifcsverchok --- src/ifcsverchok/__init__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index 66a9563c29..e0c4c1aa86 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -32,6 +32,26 @@ import importlib import logging logger = logging.getLogger("sverchok.ifc") + + +def ensure_addons_are_enabled(*addon_names: str) -> None: + errors = [] + for addon_name in addon_names: + try: + module = importlib.import_module(addon_name) + # `__addon_enabled__` is not present if addon wasn't enabled before + if not getattr(module, "__addon_enabled__", False): + errors.append(f"- Addon {addon_name} appears to be disabled, it should be enabled before IFC Sverchok.") + except ModuleNotFoundError: + errors.append(f"- Addon {addon_name} is not installed.") + + if errors: + raise Exception("Some issues were found trying to enable IFC Sverchok:\n" + "\n".join(errors)) + + +ensure_addons_are_enabled("blenderbim", "sverchok") + + from sverchok.ui.nodeview_space_menu import add_node_menu From b158a83644e20b7d4da59ab857d76b265bd705f8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Apr 2024 12:25:15 +0500 Subject: [PATCH 42/49] update ifcsverchok builds --- src/ifcopenshell-python/docs/ifcsverchok/installation.rst | 2 +- src/ifcsverchok/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst index 775ab2bcb0..b49ff5e600 100644 --- a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst +++ b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst @@ -12,7 +12,7 @@ Packaged installation IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. `Download IfcSverchok here -`__. +`__. Like all Blender add-ons, they can be installed using ``Edit > Preferences > Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox``. You can diff --git a/src/ifcsverchok/README.md b/src/ifcsverchok/README.md index cdc605f17b..401bcdfd41 100644 --- a/src/ifcsverchok/README.md +++ b/src/ifcsverchok/README.md @@ -5,7 +5,7 @@ N.B.! IfcSverchok nodes are WIP. You can experience Blender crashes while using ## Packaged installation[](https://blenderbim.org/docs-python/ifcsverchok/installation.html#packaged-installation "Permalink to this headline") -IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://blenderbim.org/builds/ifcsverchok-230823.zip). +IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://github.com/IfcOpenShell/IfcOpenShell/releases/download/ifcsverchok-240417/ifcsverchok-240417.zip). Like all Blender add-ons, they can be installed using `Edit > Preferences > Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox`. You can enable add-ons permanently by using `Save User Settings` from the Addons menu. From a748dda3788734136b73f2c42af7233f6d41f34d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Apr 2024 12:32:36 +0500 Subject: [PATCH 43/49] mention IfcSverchok in docs --- src/blenderbim/docs/users/other_addons.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/docs/users/other_addons.rst b/src/blenderbim/docs/users/other_addons.rst index 88d4c4ff7a..7ae799a6fb 100644 --- a/src/blenderbim/docs/users/other_addons.rst +++ b/src/blenderbim/docs/users/other_addons.rst @@ -29,7 +29,9 @@ Some of these add-ons are not shipped with Blender: - `Sverchok `__ - Sverchok is a visual programming add-on for Blender that allows you to generate parametric geometry, create scripts for non-programmers, model solids from FreeCAD, and - much more. + much more. There is also + `IfcSverchok `__ + that adds IFC features to Sverchok. - `BlenderGIS `__ - BlenderGIS lets you import GIS data, grab elevation data from the web, and generate TINs from survey points and contours. From 6f8dcc637fd5fb4168b362e052f0048101537918 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Wed, 17 Apr 2024 11:52:00 +0200 Subject: [PATCH 44/49] #4532 : You can now store clipping planes to a json file and restore them The file is stored along the ifc file --- .../blenderbim/bim/module/project/__init__.py | 2 + .../blenderbim/bim/module/project/operator.py | 60 +++++++++++++++++++ src/blenderbim/blenderbim/bim/ui.py | 4 +- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index ceafc1fa70..ec1008cd33 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -25,6 +25,8 @@ classes = ( operator.AppendLibraryElement, operator.AppendLibraryElementByQuery, operator.AssignLibraryDeclaration, + operator.BIM_OT_load_clipping_planes, + operator.BIM_OT_save_clipping_planes, operator.ChangeLibraryElement, operator.CreateClippingPlane, operator.CreateProject, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 8e96df2dcc..7449f8bcad 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -39,6 +39,8 @@ from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ui import IFCFileSelector from blenderbim.bim import import_ifc from blenderbim.bim import export_ifc +from collections import defaultdict +import json from math import radians from pathlib import Path from mathutils import Vector, Matrix @@ -1999,3 +2001,61 @@ class FlipClippingPlane(bpy.types.Operator): obj.rotation_euler[0] += radians(180) context.view_layer.update() return {"FINISHED"} + + +CLIPPING_PLANES_FILE_NAME = "ClippingPlanes.json" # TODO un-hardcode := + + +class BIM_OT_save_clipping_planes(bpy.types.Operator): + bl_idname = "bim.save_clipping_planes" + bl_label = "Save Clipping Planes" + bl_description = "Save Clipping Planes to Disk" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if IfcStore.path: + return context.scene.BIMProjectProperties.clipping_planes + cls.poll_message_set("Please Save The IFC File") + + def execute(self, context): + clipping_planes_to_serialize = defaultdict(dict) + clipping_planes = context.scene.BIMProjectProperties.clipping_planes + for clipping_plane in clipping_planes: + obj = clipping_plane.obj + name = obj.name + clipping_planes_to_serialize[name]["location"] = obj.location[0:3] + clipping_planes_to_serialize[name]["rotation"] = obj.rotation_euler[0:3] + with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "w") as file: + json.dump(clipping_planes_to_serialize, file, indent=4) + return {"FINISHED"} + + +class BIM_OT_load_clipping_planes(bpy.types.Operator): + bl_idname = "bim.load_clipping_planes" + bl_label = "Load Clipping Planes" + bl_description = "Load Clipping Planes from Disk" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if filepath := IfcStore.path: + if Path(filepath).with_name(CLIPPING_PLANES_FILE_NAME).exists(): + return True + else: + cls.poll_message_set(f"No Clipping Planes File in Folder {filepath}") + else: + cls.poll_message_set("Please Save The IFC File") + + def execute(self, context): + bpy.data.batch_remove(context.scene.BIMProjectProperties.clipping_planes_objs) + context.scene.BIMProjectProperties.clipping_planes.clear() + with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "r") as file: + clipping_planes_dict = json.load(file) + for name, values in clipping_planes_dict.items(): + bpy.ops.bim.create_clipping_plane() + obj = context.scene.BIMProjectProperties.clipping_planes_objs[-1] + obj.name = name + obj.location = values["location"] + obj.rotation_euler = values["rotation"] + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index bed2349191..a6345523b6 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -131,6 +131,8 @@ class BIM_PT_section_with_cappings(Panel): box = layout.box() header = box.row(align=True) header.label(text="Clipping Planes") + header.operator("bim.save_clipping_planes", text="", icon="EXPORT") + header.operator("bim.load_clipping_planes", text="", icon="IMPORT") header.operator("bim.create_clipping_plane", text="", icon="ADD") box.template_list( @@ -150,7 +152,7 @@ class BIM_PT_section_with_cappings(Panel): class BIM_UL_clipping_plane(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: + if item and item.obj: obj = item.obj row = layout.row(align=True) row.prop(obj, "name", text="", emboss=False) From f93d79dce0d874baf843cb83fd59a3c37e734a30 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Apr 2024 12:33:25 +0500 Subject: [PATCH 45/49] entity_instance.is_a overload typing --- .../ifcopenshell/entity_instance.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py index 39a703ae51..00e441d9ac 100644 --- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py +++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py @@ -30,7 +30,7 @@ import functools import subprocess import sys import time -from typing import Union, Any, Callable, TypeVar +from typing import Union, Any, Callable, TypeVar, overload from . import ifcopenshell_wrapper from . import settings @@ -317,15 +317,25 @@ class entity_instance(object): return self.wrapped_data.to_string(valid_spf) - def is_a(self, *args) -> Union[str, bool]: + @overload + def is_a(self) -> str: ... + @overload + def is_a(self, ifc_class: str) -> bool: ... + @overload + def is_a(self, with_schema: bool) -> str: ... + def is_a(self, *args: Union[str, bool]) -> Union[str, bool]: """Return the IFC class name of an instance, or checks if an instance belongs to a class. The check will also return true if a parent class name is provided. :param args: If specified, is a case insensitive IFC class name to check - :type args: string + or if specified as a boolean then will define whether + returned IFC class name should include schema name + (e.g. "IFC4.IfcWall" if `True` and "IfcWall" if `False`). + If omitted will act as `False`. + :type args: Union[str, bool] :returns: Either the name of the class, or a boolean if it passes the check - :rtype: string|bool + :rtype: Union[str, bool] Example: From 45dacc36127d6a01e9d4a88bc668a822e763708e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Apr 2024 13:34:45 +0500 Subject: [PATCH 46/49] migrate docs urls --- aws/lambda/Dockerfile | 2 +- choco/blenderbim/blenderbim.nuspec | 2 +- src/ifcbimtester/CITATION.cff | 2 +- src/ifcopenshell-python/docs/ifccsv.rst | 2 +- src/ifcopenshell-python/docs/ifcdiff.rst | 2 +- .../docs/ifcopenshell-python/installation.rst | 4 ++-- .../ifcopenshell/api/georeference/edit_georeferencing.py | 2 +- src/ifcopenshell-python/ifcopenshell/api/owner/settings.py | 4 ++-- src/ifcsverchok/README.md | 2 +- src/opencdeserver/README.md | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/aws/lambda/Dockerfile b/aws/lambda/Dockerfile index fcffa1e87b..8076692e81 100644 --- a/aws/lambda/Dockerfile +++ b/aws/lambda/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get -y update && apt-get -y install unzip curl # Install AWS Lambda runtime interface client RUN pip install --target ${FUNCTION_DIR} awslambdaric -# Set the IfcOpenShell build version (check available builds at: https://blenderbim.org/docs-python/ifcopenshell-python/installation.html) +# Set the IfcOpenShell build version (check available builds at: https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) ARG IFC_OPENSHELL_BUILD="39-v0.7.0-476ab50" # Download and extract IfcOpenShell diff --git a/choco/blenderbim/blenderbim.nuspec b/choco/blenderbim/blenderbim.nuspec index d76e86a8ce..bafb86e381 100644 --- a/choco/blenderbim/blenderbim.nuspec +++ b/choco/blenderbim/blenderbim.nuspec @@ -15,7 +15,7 @@ https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/COPYING true https://github.com/IfcOpenShell/IfcOpenShell - https://blenderbim.org/docs/ + https://docs.blenderbim.org/ https://github.com/IfcOpenShell/IfcOpenShell/issues blender bim blenderbim ifc python opensource foss diff --git a/src/ifcbimtester/CITATION.cff b/src/ifcbimtester/CITATION.cff index d2d6ca6abb..a7222b5a28 100644 --- a/src/ifcbimtester/CITATION.cff +++ b/src/ifcbimtester/CITATION.cff @@ -11,7 +11,7 @@ authors: - name: "IfcOpenShell contributors"authors: repository-code: >- https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcbimtester -url: 'https://blenderbim.org/docs-python/bimtester.html' +url: 'https://docs.ifcopenshell.org/bimtester.html' abstract: Wrapper for Gherkin based unit testing for IFC models keywords: - Gherkin diff --git a/src/ifcopenshell-python/docs/ifccsv.rst b/src/ifcopenshell-python/docs/ifccsv.rst index 76742b3ec7..3c9f93b766 100644 --- a/src/ifcopenshell-python/docs/ifccsv.rst +++ b/src/ifcopenshell-python/docs/ifccsv.rst @@ -153,7 +153,7 @@ interface to access the IfcOpenShell utilities. 1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on installation documentation - `_. + `_. 2. Launch Blender. Change to the **Scene Properties** tab in the **Properties Panel**. Scroll down to the **IFC Collaboration > IFC CSV Import / Export** diff --git a/src/ifcopenshell-python/docs/ifcdiff.rst b/src/ifcopenshell-python/docs/ifcdiff.rst index b8b5332566..1f82d4d400 100644 --- a/src/ifcopenshell-python/docs/ifcdiff.rst +++ b/src/ifcopenshell-python/docs/ifcdiff.rst @@ -88,7 +88,7 @@ interface to access the IfcOpenShell utilities. 1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on installation documentation - `_. + `_. 2. Launch Blender. Change to the **Scene Properties** tab in the **Properties Panel**. Scroll down to the **IFC Quality Control > IFC Diff** panel. diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst index 1801ccf75e..77f71fbf34 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst @@ -227,7 +227,7 @@ The BlenderBIM Add-on is available either as a stable build or a daily build. 1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on installation documentation - `_. + `_. 2. Launch Blender. On the top left of the Viewport panel, click the **Editor Type** icon to change the viewport into a **Python Console**. @@ -261,7 +261,7 @@ and run your script using the **Text > Run Script** menu or by clicking on the Blender. This can help when learning how to write scripts as you can double check the results of your scripts with what you see in the graphical interface. `Read more - `_. + `_. From source with precompiled binaries ------------------------------------- diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index 9e34626e4a..77554e1f7f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -26,7 +26,7 @@ class Usecase: surveyor, and a third-party digital engineer with expertise in IFC to moderate. For more information, read the BlenderBIM Add-on documentation for Georeferencing: - https://blenderbim.org/docs/users/georeferencing.html + https://docs.blenderbim.org/users/georeferencing.html For more information about the attributes and data types of an IfcMapConversion, consult the IFC documentation. diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py index 5427dd9c23..6c0e6a456f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py @@ -38,7 +38,7 @@ def get_application(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instanc if not app and ifc.schema == "IFC2X3": raise Exception( "Please create an application to continue. See the owner.create_owner_history docs for more info." - "https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" + "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" ) return (app or [None])[0] @@ -58,7 +58,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None if not pao and ifc.schema == "IFC2X3": raise Exception( "Please create a user to continue. See the owner.create_owner_history docs for more info." - "https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" + "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" ) return (pao or [None])[0] diff --git a/src/ifcsverchok/README.md b/src/ifcsverchok/README.md index 401bcdfd41..e2c5944dfa 100644 --- a/src/ifcsverchok/README.md +++ b/src/ifcsverchok/README.md @@ -3,7 +3,7 @@ N.B.! IfcSverchok nodes are WIP. You can experience Blender crashes while using them, especially if you're undoing (ctrl/cmd-Z) something in the node tree. -## Packaged installation[](https://blenderbim.org/docs-python/ifcsverchok/installation.html#packaged-installation "Permalink to this headline") +## Packaged installation[](https://docs.ifcopenshell.org/ifcsverchok/installation.html#packaged-installation "Permalink to this headline") IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://github.com/IfcOpenShell/IfcOpenShell/releases/download/ifcsverchok-240417/ifcsverchok-240417.zip). diff --git a/src/opencdeserver/README.md b/src/opencdeserver/README.md index 2623000fbc..87d2fb8147 100644 --- a/src/opencdeserver/README.md +++ b/src/opencdeserver/README.md @@ -250,7 +250,7 @@ https://technical.buildingsmart.org/standards/ifc/ifc-schema-specifications/ https://www.sciencedirect.com/science/article/pii/S0926580523000389 - IFCOpenShell documentation -https://blenderbim.org/docs-python/ +https://docs.ifcopenshell.org/ ### Frontend From 3b637a023460afb37fd85402954f967a7701a2b0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Apr 2024 15:18:22 +0500 Subject: [PATCH 47/49] ifcsverchok - autogenerate node categories from index --- src/ifcsverchok/__init__.py | 39 +++++++++---------------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index e0c4c1aa86..8b45c3f712 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -89,36 +89,15 @@ def nodes_index(): ] -node_categories = [ - { - "IFC": [ - "SvIfcCreateFile", - "SvIfcReadFile", - "SvIfcWriteFile", - "SvIfcCreateEntity", - "SvIfcCreateShape", - "SvIfcReadEntity", - "SvIfcPickIfcClass", - "SvIfcById", - "SvIfcByGuid", - "SvIfcByType", - "SvIfcByQuery", - "SvIfcAdd", - "SvIfcAddPset", - "SvIfcAddSpatialElement", - "SvIfcRemove", - "SvIfcGenerateGuid", - "SvIfcGetProperty", - "SvIfcGetAttribute", - "SvIfcSelectBlenderObjects", - "SvIfcApi", - "SvIfcBMeshToIfcRepr", - "SvIfcSverchokToIfcRepr", - "SvIfcCreateProject", - "SvIfcQuickProjectSetup", - ] - } -] +def make_node_categories() -> list[dict[str, list[str]]]: + node_categories = [{}] + for category, nodes in nodes_index(): + nodes = [node_name for idname, node_name in nodes] + node_categories[0][category] = nodes + return node_categories + + +node_categories = make_node_categories() def make_node_list(): From ea6f0b054a658cd2c93248cfb5d60bf27a845144 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Wed, 17 Apr 2024 21:42:51 -0500 Subject: [PATCH 48/49] fixes: #4563 --- src/blenderbim/blenderbim/bim/import_ifc.py | 6 +++++- src/blenderbim/blenderbim/bim/ui.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index a809831f3a..02173e2681 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -857,7 +857,11 @@ class IfcImporter: print("Done creating geometry") def create_spatial_elements(self) -> None: - self.create_generic_elements(self.spatial_elements, unselectable=True) + if bpy.context.preferences.addons["blenderbim"].preferences.spatial_elements_unselectable: + self.create_generic_elements(self.spatial_elements, unselectable=True) + else: + self.create_generic_elements(self.spatial_elements, unselectable=False) + def create_elements(self) -> None: self.create_generic_elements(self.elements) diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index a6345523b6..7654062959 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -204,6 +204,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False ) lock_grids_on_import: BoolProperty(name="Should Lock Grids By Default", default=True) + spatial_elements_unselectable: BoolProperty(name="Should Make Spatial Elements Unselectable By Default", default=True) decorations_colour: bpy.props.FloatVectorProperty( name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4 ) @@ -285,6 +286,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row.prop(self, "should_play_chaching_sound") row = layout.row() row.prop(self, "lock_grids_on_import") + row = layout.row() + row.prop(self, "spatial_elements_unselectable") + + row = layout.row() row.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save") From fa4239159df9b074b25a5a2ed04b54f625012484 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 18 Apr 2024 23:39:17 +1000 Subject: [PATCH 49/49] Fix incorrect dateutil dependency --- src/ifcopenshell-python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/pyproject.toml b/src/ifcopenshell-python/pyproject.toml index 89a55a2c69..fc1bff6807 100644 --- a/src/ifcopenshell-python/pyproject.toml +++ b/src/ifcopenshell-python/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", ] -dependencies = ["mathutils", "shapely", "numpy", "isodate", "dateutil", "lark"] +dependencies = ["mathutils", "shapely", "numpy", "isodate", "python-dateutil", "lark"] [project.urls] "Homepage" = "http://ifcopenshell.org"