From 36754293853061a32c520221d6dd638626569fd3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 3 Sep 2024 18:42:47 +1000 Subject: [PATCH] Fix #2437. You can now see the spatial hierarchy decomposed by classification. --- .../bonsai/bim/module/spatial/operator.py | 77 ++++++++---- src/bonsai/bonsai/bim/module/spatial/prop.py | 3 + src/bonsai/bonsai/bim/module/spatial/ui.py | 16 +-- src/bonsai/bonsai/core/spatial.py | 13 -- src/bonsai/bonsai/tool/spatial.py | 119 +++++++++++++++++- 5 files changed, 171 insertions(+), 57 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index a592eb8843..007b4716e5 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -293,34 +293,61 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): return self.execute(context) def _execute(self, context): - ifc_class = relating_type = None - is_untyped = False ifc_file = tool.Ifc.get() - if self.should_filter: - active_element = context.scene.BIMSpatialDecompositionProperties.active_element - element_type = active_element.type - if element_type == "CLASS": - ifc_class = active_element.name - elif element_type == "TYPE": - ifc_class = active_element.ifc_class - if ifc_id := active_element.ifc_definition_id: - relating_type = ifc_file.by_id(ifc_id) - else: # OCCURRENCE - occurrence = ifc_file.by_id(active_element.ifc_definition_id) - obj = tool.Ifc.get_object(occurrence) - assert isinstance(obj, bpy.types.Object) - tool.Blender.set_active_object(obj) + container = ifc_file.by_id(self.container) + props = context.scene.BIMSpatialDecompositionProperties + element_filter = props.element_filter + active_element = props.active_element + + if not self.should_filter and not element_filter: + tool.Spatial.select_products(tool.Spatial.get_decomposed_elements(container)) + return + + if props.element_mode == "TYPE": + if active_element.type == "OCCURRENCE": + if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)): + tool.Blender.set_active_object(obj) return - element_filter = context.scene.BIMSpatialDecompositionProperties.element_filter - core.select_decomposed_elements( - tool.Spatial, - container=ifc_file.by_id(self.container), - ifc_class=ifc_class, - relating_type=relating_type, - is_untyped=is_untyped, - element_filter=element_filter, - ) + ifc_class = relating_type = None + is_untyped = False + + if self.should_filter: + if active_element.type == "CLASS": + ifc_class = active_element.name + elif active_element.type == "TYPE": + ifc_class = active_element.ifc_class + if ifc_id := active_element.ifc_definition_id: + relating_type = ifc_file.by_id(ifc_id) + + elements = tool.Spatial.get_decomposed_elements(container) + elements = tool.Spatial.filter_elements(elements, ifc_class, relating_type, is_untyped, element_filter) + tool.Spatial.select_products(elements) + elif props.element_mode == "DECOMPOSITION": + occurrence = ifc_file.by_id(active_element.ifc_definition_id) + elements = ifcopenshell.util.element.get_decomposition(occurrence) + elements.add(occurrence) + tool.Spatial.select_products(elements) + elif props.element_mode == "CLASSIFICATION": + if active_element.type == "OCCURRENCE": + if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)): + tool.Blender.set_active_object(obj) + return + + if active_element.type == "CLASSIFICATION": + identification = active_element.identification + elements = tool.Spatial.get_decomposed_elements(container) + + def filter_element(element: ifcopenshell.entity_instance) -> bool: + references = ifcopenshell.util.classification.get_references(element) + if identification == "Unclassified": + if not references: + return True + elif any([r for r in references if r[1].startswith(identification)]): + return True + return False + + tool.Spatial.select_products(filter(filter_element, elements)) class SetDefaultContainer(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 8f46411b6e..2e4a7d1881 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -112,6 +112,7 @@ class BIMContainer(PropertyGroup): class Element(PropertyGroup): name: StringProperty(name="Name") ifc_class: StringProperty(name="Name", description="Type or element IFC class, empty if 'type' is 'CLASS'") + identification: StringProperty(name="Identification", description="Identification if 'type' is 'CLASSIFICATION'") ifc_definition_id: IntProperty( name="IFC Definition ID", description="ID of the element type / occurrence. 0 if 'type' is 'CLASS' or if it's 'TYPE' but it represents untyped elements", @@ -125,6 +126,7 @@ class Element(PropertyGroup): items=( ("CLASS", "CLASS", "CLASS"), ("TYPE", "TYPE", "TYPE"), + ("CLASSIFICATION", "CLASSIFICATION", "CLASSIFICATION"), ("OCCURRENCE", "OCCURRENCE", "OCCURRENCE"), ), ) @@ -145,6 +147,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup): items=( ("TYPE", "Class > Type > Occurrence", "Show a breakdown by IfcClass, relating type, and occurrence"), ("DECOMPOSITION", "Element Decomposition", "Show a breakdown by element decomposition"), + ("CLASSIFICATION", "Element Classification", "Show a breakdown by element classification"), ), update=update_element_mode, ) diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index 6ee712d4dd..d7f9569ace 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -278,27 +278,15 @@ class BIM_UL_elements(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag): if item: row = layout.row(align=True) - item_type = item.type for _ in range(item.level): row.label(text="", icon="BLANK1") if item.has_children: self.draw_toggle(row, item.is_expanded, index) - if item_type == "CLASS": - row.label(text=item.name) + row.label(text=item.name) + if item.total: col = row.column() col.alignment = "RIGHT" col.label(text=str(item.total)) - elif item_type == "TYPE": - row.label(text=item.name) - col = row.column() - col.alignment = "RIGHT" - col.label(text=str(item.total)) - elif item_type == "OCCURRENCE": - row.label(text=item.name) - if item.total: - col = row.column() - col.alignment = "RIGHT" - col.label(text=str(item.total)) def draw_filter(self, context, layout): row = layout.row() diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index f813d21709..bd1ba68d3a 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -146,19 +146,6 @@ def toggle_container_element(spatial: tool.Spatial, element_index: int, is_recur spatial.load_contained_elements() -def select_decomposed_elements( - spatial: tool.Spatial, - container: ifcopenshell.entity_instance, - ifc_class: str | None = None, - relating_type: ifcopenshell.entity_instance | None = None, - is_untyped: bool = False, - element_filter: str | None = None, -) -> None: - elements = spatial.get_decomposed_elements(container) - elements = spatial.filter_elements(elements, ifc_class, relating_type, is_untyped, element_filter) - spatial.select_products(elements) - - def select_decomposed_element(ifc: tool.Ifc, spatial: tool.Spatial, element: ifcopenshell.entity_instance) -> None: spatial.set_active_object(ifc.get_object(element)) diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 8f23f71bd3..7d484eb972 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -26,6 +26,7 @@ import ifcopenshell.geom import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation +import ifcopenshell.util.classification import ifcopenshell.util.shape_builder import ifcopenshell.util.type import ifcopenshell.util.unit @@ -199,7 +200,52 @@ class Spatial(bonsai.core.tool.Spatial): src_obj.location.xy = destination_obj.location.xy @classmethod - def get_grouped_elements_in_container(cls, container: ifcopenshell.entity_instance) -> dict: + def get_container_elements_grouped_by_classification(cls, container: ifcopenshell.entity_instance) -> dict: + props = bpy.context.scene.BIMSpatialDecompositionProperties + results = {} + if props.should_include_children: + elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True) + else: + elements = ifcopenshell.util.element.get_contained(container) + flat_results: dict[str, Any] = {} + reference_names: dict[str, str] = {} + for element in elements: + if element.is_a("IfcOpeningElement") or tool.Root.is_spatial_element(element): + continue + references = ifcopenshell.util.classification.get_references(element) + if not references: + flat_results.setdefault("Unclassified", []).append(element) + else: + for reference in references: + identification = reference[1] or "" + reference_names[identification] = reference[2] + flat_results.setdefault(identification, []).append(element) + + for flat_key in sorted(flat_results.keys()): + current_results = results + + while True: + has_parent = None + for key in current_results: + if flat_key.startswith(key): + has_parent = True + new_current_results = current_results[key]["children"] + break + if has_parent: + current_results = new_current_results + else: + break + + current_results[flat_key] = { + "Name": "Unclassified" if flat_key == "Unclassified" else reference_names[flat_key], + "elements": flat_results[flat_key], + "children": {}, + } + + return results + + @classmethod + def get_container_elements_grouped_by_type(cls, container: ifcopenshell.entity_instance) -> dict: props = bpy.context.scene.BIMSpatialDecompositionProperties results: defaultdict[str, dict[int, Any]] = defaultdict(dict) if props.should_include_children: @@ -230,11 +276,13 @@ class Spatial(bonsai.core.tool.Spatial): cls.load_contained_elements_by_type(container) elif props.element_mode == "DECOMPOSITION": cls.load_contained_elements_by_decomposition(container) + elif props.element_mode == "CLASSIFICATION": + cls.load_contained_elements_by_classification(container) @classmethod def load_contained_elements_by_type(cls, container: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMSpatialDecompositionProperties - results = cls.get_grouped_elements_in_container(container) + results = cls.get_container_elements_grouped_by_type(container) expanded_elements = json.loads(props.expanded_elements) expanded_classes = expanded_elements.get("CLASS", []) @@ -295,9 +343,8 @@ class Spatial(bonsai.core.tool.Spatial): def add_elements(elements, level=0): for element in sorted(elements, key=lambda x: f"{x.is_a()}/{x.Name or 'Unnamed'}"): ifc_definition_id = element.id() - name = f"{element.is_a()}/{element.Name or 'Unnamed'}" new = props.elements.add() - new.name = name + new.name = f"{element.is_a()}/{element.Name or 'Unnamed'}" new.level = level new.ifc_definition_id = ifc_definition_id new.type = "OCCURRENCE" @@ -310,11 +357,46 @@ class Spatial(bonsai.core.tool.Spatial): new.has_children = True new.total = len(children) if ifc_definition_id in expanded_ifc_ids: + new.is_expanded = True add_elements(children, level=level + 1) elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=False) add_elements(elements) + @classmethod + def load_contained_elements_by_classification(cls, container: ifcopenshell.entity_instance) -> None: + props = bpy.context.scene.BIMSpatialDecompositionProperties + expanded_elements = json.loads(props.expanded_elements) + expanded_classifications = expanded_elements.get("CLASSIFICATION", []) + expanded_classifications_r = expanded_elements.get("CLASSIFICATION_R", []) + + def add_elements(results, level=0): + for identification in sorted(results.keys()): + data = results[identification] + new = props.elements.add() + new.name = f"{identification}:{data['Name']}" + new.type = "CLASSIFICATION" + new.identification = identification + new.has_children = True + new.level = level + + is_expanded = identification in expanded_classifications + is_expanded_r = any([c for c in expanded_classifications_r if identification.startswith(c)]) + + if is_expanded or is_expanded_r: + new.is_expanded = True + add_elements(data["children"], level=level + 1) + + for element in sorted(data["elements"], key=lambda x: f"{x.is_a()}/{x.Name or 'Unnamed'}"): + new2 = props.elements.add() + new2.name = f"{element.is_a()}/{element.Name or 'Unnamed'}" + new2.type = "OCCURRENCE" + new2.level = level + 1 + new2.ifc_definition_id = element.id() + + results = cls.get_container_elements_grouped_by_classification(container) + add_elements(results) + @classmethod def filter_elements( cls, @@ -418,6 +500,8 @@ class Spatial(bonsai.core.tool.Spatial): cls.toggle_container_element_by_type(element_index, is_recursive) elif props.element_mode == "DECOMPOSITION": cls.toggle_container_element_by_decomposition(element_index, is_recursive) + elif props.element_mode == "CLASSIFICATION": + cls.toggle_container_element_by_classification(element_index, is_recursive) @classmethod def toggle_container_element_by_type(cls, element_index: int, is_recursive: bool) -> None: @@ -448,7 +532,7 @@ class Spatial(bonsai.core.tool.Spatial): if is_recursive and element.type == "CLASS": container = tool.Ifc.get().by_id(props.active_container.ifc_definition_id) - results = cls.get_grouped_elements_in_container(container) + results = cls.get_container_elements_grouped_by_type(container) for ifc_class in results.keys(): if ifc_class != element.name: continue @@ -501,6 +585,31 @@ class Spatial(bonsai.core.tool.Spatial): props.expanded_elements = json.dumps(expanded_elements) + @classmethod + def toggle_container_element_by_classification(cls, element_index: int, is_recursive: bool) -> None: + props = bpy.context.scene.BIMSpatialDecompositionProperties + expanded_elements: dict[str, list[str]] = json.loads(props.expanded_elements) + expanded_elements_list: list[str] = expanded_elements.setdefault("CLASSIFICATION", []) + + element = props.elements[element_index] + identification = element.identification + + if identification in expanded_elements_list: + expanded_elements_list.remove(identification) + should_expand = False + else: + expanded_elements_list.append(identification) + should_expand = True + + if is_recursive: + expanded_elements_list = expanded_elements.setdefault("CLASSIFICATION_R", []) + if should_expand is False and identification in expanded_elements_list: + expanded_elements_list.remove(identification) + elif should_expand is True and identification not in expanded_elements_list: + expanded_elements_list.append(identification) + + props.expanded_elements = json.dumps(expanded_elements) + # HERE STARTS SPATIAL TOOL @classmethod