From ba7741fef5568d434352be5d2a355a597b3e0995 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 12 Jan 2023 12:28:09 +1100 Subject: [PATCH] Fix #2285. You can now get all the elements in a presentation layer. --- .../blenderbim/bim/module/layer/__init__.py | 13 +- .../blenderbim/bim/module/layer/operator.py | 20 ++- .../blenderbim/bim/module/layer/ui.py | 6 + .../ifcopenshell/util/element.py | 161 +++++++++++++++--- .../test/util/test_element.py | 14 +- 5 files changed, 183 insertions(+), 31 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/layer/__init__.py b/src/blenderbim/blenderbim/bim/module/layer/__init__.py index 80cedcd07c..697a3ee038 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/layer/__init__.py @@ -20,14 +20,15 @@ import bpy from . import ui, prop, operator classes = ( - operator.LoadLayers, - operator.DisableLayerEditingUI, - operator.EnableEditingLayer, - operator.DisableEditingLayer, operator.AddPresentationLayer, - operator.EditPresentationLayer, - operator.RemovePresentationLayer, operator.AssignPresentationLayer, + operator.DisableEditingLayer, + operator.DisableLayerEditingUI, + operator.EditPresentationLayer, + operator.EnableEditingLayer, + operator.LoadLayers, + operator.RemovePresentationLayer, + operator.SelectLayerProducts, operator.UnassignPresentationLayer, prop.Layer, prop.BIMLayerProperties, diff --git a/src/blenderbim/blenderbim/bim/module/layer/operator.py b/src/blenderbim/blenderbim/bim/module/layer/operator.py index 2c20351d20..a28d446904 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/operator.py +++ b/src/blenderbim/blenderbim/bim/module/layer/operator.py @@ -18,9 +18,11 @@ import bpy import json -import ifcopenshell.util.attribute import ifcopenshell.api +import ifcopenshell.util.element +import ifcopenshell.util.attribute import blenderbim.bim.helper +import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.layer.data import Data @@ -182,3 +184,19 @@ class UnassignPresentationLayer(bpy.types.Operator): ) Data.load(IfcStore.get_file()) return {"FINISHED"} + + +class SelectLayerProducts(bpy.types.Operator): + bl_idname = "bim.select_layer_products" + bl_label = "Select Layer Products" + bl_options = {"REGISTER", "UNDO"} + layer: bpy.props.IntProperty() + + def execute(self, context): + elements = ifcopenshell.util.element.get_elements_by_layer(tool.Ifc.get(), tool.Ifc.get().by_id(self.layer)) + for obj in context.visible_objects: + obj.select_set(False) + element = tool.Ifc.get_entity(obj) + if element and element in elements: + obj.select_set(True) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/layer/ui.py b/src/blenderbim/blenderbim/bim/module/layer/ui.py index 05dc1825a1..f86c4dbf63 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/ui.py +++ b/src/blenderbim/blenderbim/bim/module/layer/ui.py @@ -91,11 +91,17 @@ class BIM_UL_layers(UIList): row.operator("bim.disable_editing_layer", text="", icon="FREEZE", emboss=False) if context.scene.BIMLayerProperties.active_layer_id == item.ifc_definition_id: + op = row.operator("bim.select_layer_products", text="", icon="RESTRICT_SELECT_OFF") + op.layer = item.ifc_definition_id row.operator("bim.edit_presentation_layer", text="", icon="CHECKMARK") row.operator("bim.disable_editing_layer", text="", icon="CANCEL") elif context.scene.BIMLayerProperties.active_layer_id: + op = row.operator("bim.select_layer_products", text="", icon="RESTRICT_SELECT_OFF") + op.layer = item.ifc_definition_id row.operator("bim.remove_presentation_layer", text="", icon="X").layer = item.ifc_definition_id else: + op = row.operator("bim.select_layer_products", text="", icon="RESTRICT_SELECT_OFF") + op.layer = item.ifc_definition_id op = row.operator("bim.enable_editing_layer", text="", icon="GREASEPENCIL") op.layer = item.ifc_definition_id row.operator("bim.remove_presentation_layer", text="", icon="X").layer = item.ifc_definition_id diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 7cc76f8688..9c79156091 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -31,6 +31,7 @@ def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True): Example: .. code:: python + element = ifcopenshell.by_type("IfcBuildingElement")[0] psets = ifcopenshell.util.element.get_psets(element, psets_only=True) qsets = ifcopenshell.util.element.get_psets(element, qtos_only=True) @@ -120,17 +121,24 @@ def get_properties(properties): def get_predefined_type(element): - """ - Retrieves the PrefefinedType attribute of an element. + """Retrieves the PrefefinedType attribute of an element. + + If the predefined type is user defined, the custom type (such as object + type, element type, or process type depending on the class) is returned + instead. Predefined types from the associated type element are also + considered first. :param element: The IFC Element entity + :type element: ifcopenshell.entity_instance.entity_instance :return: The predefined type of the element + :rtype: str Example: .. code:: python - element = ifcopenshell.by_type("IfcWall")[0] - predefined_type = ifcopenshell.util.element.get_predefined_type(element) + + element = ifcopenshell.by_type("IfcWall")[0] + predefined_type = ifcopenshell.util.element.get_predefined_type(element) """ element_type = get_type(element) if element_type: @@ -146,17 +154,19 @@ def get_predefined_type(element): def get_type(element): - """ - Retrieves the Element Type entity related to an element entity. + """Retrieves the construction type element of an element occurrence - :param element: The IFC Element entity - :return: The Element Type entity defining the element + :param element: The element occurrence + :type: ifcopenshell.entity_instance.entity_instance + :return: The related type element + :rtype ifcopenshell.entity_instance.entity_instance Example: .. code:: python - element = ifcopenshell.by_type("IfcWall")[0] - element_type = ifcopenshell.util.element.get_type(element) + + element = ifcopenshell.by_type("IfcWall")[0] + element_type = ifcopenshell.util.element.get_type(element) """ if element.is_a("IfcTypeObject"): return element @@ -169,6 +179,20 @@ def get_type(element): def get_types(type): + """Get all the occurrences of a type element + + :param type: The type element + :type type: ifcopenshell.entity_instance.entity_instance + :return: A list of occurrences of that type + :rtype: list[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + element_type = ifcopenshell.by_type("IfcWallType")[0] + walls = ifcopenshell.util.element.get_types(element_type) + """ for rel in getattr(type, "Types", []): return rel.RelatedObjects for rel in getattr(type, "ObjectTypeOf", []): @@ -177,6 +201,29 @@ def get_types(type): def get_material(element, should_skip_usage=False, should_inherit=True): + """Gets the material of the element + + The material may be a single material, material set (layered, profiled, or + constituent), or a material set usage. + + :param should_skip_usage: If set to True, if the material is a material set + usage, the material set itself will be returned. Useful if you don't + care about occurrence usage parameters. If False, the usage will be + returned. + :type should_skip_usage: bool + :param should_inherit: If True, any inherited materials from associated + types will be considered. + :type should_inherit: bool + :return: The associated material of the element. + :rtype: ifcopenshell.entity_instance.entity_instance + + Example: + + .. code:: python + + element = ifcopenshell.by_type("IfcWall")[0] + material = ifcopenshell.util.element.material(element) + """ if hasattr(element, "HasAssociations") and element.HasAssociations: for relationship in element.HasAssociations: if relationship.is_a("IfcRelAssociatesMaterial"): @@ -193,15 +240,17 @@ def get_material(element, should_skip_usage=False, should_inherit=True): def get_elements_by_material(ifc_file, material): - """ - Retrieves the elements related to a material. + """Retrieves the elements related to a material. This includes elements using the material as part of a material set or set usage. :param ifc_file: The IFC file + :type ifc_file: ifcopenshell.file.file :param material: The IFC Material entity + :type material: ifcopenshell.entity_instance.entity_instance :return: A list of elements using the to the material + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -233,12 +282,14 @@ def get_elements_by_material(ifc_file, material): def get_elements_by_style(ifc_file, style): - """ - Retrieves the elements related to a style. + """Retrieves the elements whose geometric representation uses a style :param ifc_file: The IFC file - :param style: The IFC Style entity + :type ifc_file: ifcopenshell.file.file + :param style: The IfcPresentationStyle entity + :type style: ifcopenshell.entity_instance.entity_instance :return: The elements related to the style + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -271,6 +322,22 @@ def get_elements_by_style(ifc_file, style): def get_elements_by_representation(ifc_file, representation): + """Gets all elements using a geometric representation + + :param ifc_file: The IFC file + :type ifc_file: ifcopenshell.file.file + :param representation: The IfcShapeRepresentation representation + :type representation: ifcopenshell.entity_instance.entity_instance + :return: The elements using the geometric representation + :rtype: list[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + representation = file.by_type("IfcShapeRepresentation")[0] + elements = ifcopenshell.util.element.get_elements_by_representation(file, representation) + """ results = set() [results.update(pr.ShapeOfProduct) for pr in representation.OfProductRepresentation] for rep_map in representation.RepresentationMap: @@ -286,7 +353,47 @@ def get_elements_by_representation(ifc_file, representation): return results +def get_elements_by_layer(ifc_file, layer): + """Get all the elements that are used by a presentation layer + + :param ifc_file: The IFC file + :type ifc_file: ifcopenshell.file.file + :param layer: The IfcPresentationLayerAssignment layer + :type layer: ifcopenshell.entity_instance.entity_instance + :return: The elements using the geometric representation + :rtype: list[ifcopenshell.entity_instance.entity_instance] + """ + results = set() + for item in layer.AssignedItems: + if item.is_a("IfcShapeRepresentation"): + results.update(get_elements_by_representation(ifc_file, item)) + elif item.is_a("IfcRepresentationItem"): + for inverse in ifc_file.get_inverse(item): + if inverse.is_a("IfcShapeRepresentation"): + results.update(get_elements_by_representation(ifc_file, inverse)) + return results + + def get_layers(ifc_file, element): + """Get the CAD layers that an element is part of + + An element may have portions or all of its geometry assigned to a + traditional CAD presentation layer. + + :param ifc_file: The IFC file object + :type ifc_file: ifcopenshell.file.file + :param element: The IFC element to interrogate + :type element: ifcopenshell.entity_instance.entity_instance + :return: A list of IfcPresentationLayerAssignment + :rtype: list[ifcopenshell.entity_instance.entity_instance] + + Example: + + .. code:: python + + element = ifcopenshell.by_type("IfcWall")[0] + layers = ifcopenshell.util.element.get_layers(element) + """ layers = [] representations = [] if getattr(element, "Representation", None): @@ -338,10 +445,11 @@ def get_container(element, should_get_direct=False): def get_referenced_structures(element): - """ - Retreives a list of referenced spatial elements, typically useful for - multistorey elements or elements that span multiple spaces or in-between - spaces. + """Retreives a list of referenced spatial elements + + Typically useful for multistorey elements, such as columns or facade + elements, or elements that span multiple spaces or in-between spaces, such + as stairs, doors, etc. :param element: The IFC element :type element: ifcopenshell.entity_instance.entity_instance @@ -469,6 +577,10 @@ def has_element_reference(value, element): def remove_deep(ifc_file, element): + """Recursively purges a subgraph safely. + + Do not use, use remove_deep2() instead. + """ # @todo maybe some sort of try-finally mechanism. ifc_file.batch() subgraph = list(ifc_file.traverse(element, breadth_first=True)) @@ -480,8 +592,7 @@ def remove_deep(ifc_file, element): def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): - """ - Recursively purges a subgraph safely, starting at an element + """Recursively purges a subgraph safely, starting at an element This should always be used instead of remove_deep. See #1812. The start element must have no inverses. The subgraph to be purged is calculated using @@ -505,6 +616,8 @@ def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): The do_not_delete argument contains all elements that may be part of the subgraph but are protected from deletion. + :param ifc_file: The IFC file object + :type ifc_file: ifcopenshell.file.file :param element: The starting element that defines the subgraph :type element: ifcopenshell.entity_instance.entity_instance """ @@ -536,8 +649,11 @@ def copy(ifc_file, element): GlobalIds are regenerated. :param ifc_file: The IFC file object + :type ifc_file: ifcopenshell.file.file :param element: The IFC element to copy + :type element: ifcopenshell.entity_instance.entity_instance :return: The newly copied element + :rtype: ifcopenshell.entity_instance.entity_instance """ new = ifc_file.create_entity(element.is_a()) for i, attribute in enumerate(element): @@ -557,11 +673,14 @@ def copy_deep(ifc_file, element, exclude=None): GlobalIds are regenerated. :param ifc_file: The IFC file object + :type ifc_file: ifcopenshell.file.file :param element: The IFC element to copy + :type element: ifcopenshell.entity_instance.entity_instance :param exclude: An optional list of strings of IFC class names to not copy. If any of the subelement is this class, it will not be copied and the original instance will be referenced. :return: The newly copied element + :rtype: ifcopenshell.entity_instance.entity_instance """ new = ifc_file.create_entity(element.is_a()) for i, attribute in enumerate(element): diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index a63275713e..9b3410646c 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -441,9 +441,7 @@ class TestGetElementsByRepresentation(test.bootstrap.IFC4): self.file.createIfcShapeRepresentation( Items=[ self.file.createIfcMappedItem( - MappingSource=self.file.createIfcRepresentationMap( - MappedRepresentation=representation - ) + MappingSource=self.file.createIfcRepresentationMap(MappedRepresentation=representation) ) ] ) @@ -458,6 +456,16 @@ class TestGetElementsByRepresentation(test.bootstrap.IFC4): assert subject.get_elements_by_representation(self.file, representation) == {element} +class TestGetElementsByLayer(test.bootstrap.IFC4): + def test_getting_the_elements_of_a_layer(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + layer = ifcopenshell.api.run("layer.add_layer", self.file) + representation = self.file.createIfcShapeRepresentation() + element.Representation = self.file.createIfcProductDefinitionShape(Representations=[representation]) + ifcopenshell.api.run("layer.assign_layer", self.file, item=representation, layer=layer) + assert list(subject.get_elements_by_layer(self.file, layer)) == [element] + + class TestGetlayers(test.bootstrap.IFC4): def test_getting_the_layer_of_a_product_representation(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")