From 760816b7f04da7c4aa16a558de898f0b0a586124 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 18:25:17 +0500 Subject: [PATCH] Show occurrences in spatial manager decomposed elements Example - https://imgur.com/a/PNFUbv0 --- .../bonsai/bim/module/spatial/operator.py | 28 +++++--- src/bonsai/bonsai/bim/module/spatial/prop.py | 19 +++-- src/bonsai/bonsai/bim/module/spatial/ui.py | 19 +++-- src/bonsai/bonsai/core/spatial.py | 4 +- src/bonsai/bonsai/tool/covering.py | 4 ++ src/bonsai/bonsai/tool/spatial.py | 69 ++++++++++++++----- 6 files changed, 102 insertions(+), 41 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 2301c97182..8d59dfa223 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -247,10 +247,10 @@ class ToggleContainerElement(bpy.types.Operator): bl_idname = "bim.toggle_container_element" bl_label = "Toggle Container Element" bl_options = {"REGISTER", "UNDO"} - ifc_class: bpy.props.StringProperty() + element_index: bpy.props.IntProperty() def execute(self, context): - core.toggle_container_element(tool.Spatial, ifc_class=self.ifc_class) + core.toggle_container_element(tool.Spatial, element_index=self.element_index) return {"FINISHED"} @@ -271,21 +271,29 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): return self.execute(context) def _execute(self, context): - ifc_class = relating_type = is_untyped = None + ifc_class = relating_type = None + is_untyped = False + ifc_file = tool.Ifc.get() if self.should_filter: active_element = context.scene.BIMSpatialDecompositionProperties.active_element - if active_element.is_class: + element_type = active_element.type + if element_type == "CLASS": ifc_class = active_element.name - elif relating_type := active_element.ifc_definition_id: + elif element_type == "TYPE": ifc_class = active_element.ifc_class - relating_type = tool.Ifc.get().by_id(relating_type) - else: - ifc_class = active_element.ifc_class - is_untyped = True + 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) + return + element_filter = context.scene.BIMSpatialDecompositionProperties.element_filter core.select_decomposed_elements( tool.Spatial, - container=tool.Ifc.get().by_id(self.container), + container=ifc_file.by_id(self.container), ifc_class=ifc_class, relating_type=relating_type, is_untyped=is_untyped, diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index ef91769928..76f539afc8 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -107,12 +107,21 @@ class BIMContainer(PropertyGroup): class Element(PropertyGroup): name: StringProperty(name="Name") - ifc_class: StringProperty(name="Name") - is_class: BoolProperty(name="Is Class", default=False) - is_type: BoolProperty(name="Is Type", default=False) - ifc_definition_id: IntProperty(name="IFC Definition ID") + ifc_class: StringProperty(name="Name", description="Type or element IFC class, empty if 'type' is 'CLASS'") + 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", + ) total: IntProperty(name="Total") is_expanded: BoolProperty(name="Is Expanded", default=False) + type: EnumProperty( + name="Element Type", + items=( + ("CLASS", "CLASS", "CLASS"), + ("TYPE", "TYPE", "TYPE"), + ("OCCURRENCE", "OCCURRENCE", "OCCURRENCE"), + ), + ) class BIMSpatialDecompositionProperties(PropertyGroup): @@ -122,7 +131,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup): active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index) element_filter: StringProperty(name="Element Filter", default="", options={"TEXTEDIT_UPDATE"}) elements: CollectionProperty(name="Elements", type=Element) - contracted_classes: StringProperty(name="Contracted Classes", default="[]") + expanded_elements: StringProperty(name="Expanded Elements", default="{}") active_element_index: IntProperty(name="Active Element Index") total_elements: IntProperty(name="Total Elements") subelement_class: bpy.props.EnumProperty(items=get_subelement_class, name="Subelement Class") diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index da35223b33..164ab89c01 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -258,22 +258,31 @@ class BIM_UL_elements(UIList): def __init__(self): self.use_filter_show = True - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_toggle(self, row: bpy.types.UILayout, is_expanded: bool, index: int): + icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT" + row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag): if item: row = layout.row(align=True) - if item.is_class: - icon_id = "DISCLOSURE_TRI_DOWN" if item.is_expanded else "DISCLOSURE_TRI_RIGHT" - row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).ifc_class = item.name + item_type = item.type + if item_type == "CLASS": + self.draw_toggle(row, item.is_expanded, index) row.label(text=item.name) col = row.column() col.alignment = "RIGHT" col.label(text=str(item.total)) - elif item.is_type: + elif item_type == "TYPE": row.label(text="", icon="BLANK1") + self.draw_toggle(row, item.is_expanded, index) row.label(text=item.name) col = row.column() col.alignment = "RIGHT" col.label(text=str(item.total)) + else: # OCCURRENCE + for _ in range(2): + row.label(text="", icon="BLANK1") + row.label(text=item.name) 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 1bf19d386e..9a200626e4 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -141,8 +141,8 @@ def delete_container( spatial.import_spatial_decomposition() -def toggle_container_element(spatial: tool.Spatial, ifc_class: str) -> None: - spatial.toggle_container_element(ifc_class) +def toggle_container_element(spatial: tool.Spatial, element_index: int) -> None: + spatial.toggle_container_element(element_index) spatial.load_contained_elements() diff --git a/src/bonsai/bonsai/tool/covering.py b/src/bonsai/bonsai/tool/covering.py index a0461e99f7..d682761ae3 100644 --- a/src/bonsai/bonsai/tool/covering.py +++ b/src/bonsai/bonsai/tool/covering.py @@ -49,6 +49,8 @@ class Covering(bonsai.core.tool.Covering): def covering_poll_wall_selected( cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str ) -> bool: + if not tool.Ifc.get(): + return False if not context.selected_objects or not context.active_object: operator.poll_message_set("No objects selected.") return False @@ -62,6 +64,8 @@ class Covering(bonsai.core.tool.Covering): def covering_poll_relating_type_check( cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str ) -> bool: + if not tool.Ifc.get(): + return False props = context.scene.BIMModelProperties relating_type_id = tool.Blender.get_enum_safe(props, "relating_type_id") if relating_type_id is not None: diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index df0ae65120..9b8cdbf46c 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -39,7 +39,8 @@ import json from math import pi from mathutils import Vector, Matrix from shapely import Polygon -from typing import Generator, Optional, Union, Literal, List +from typing import Generator, Optional, Union, Literal, List, Any, Iterable +from collections import defaultdict class Spatial(bonsai.core.tool.Spatial): @@ -139,7 +140,7 @@ class Spatial(bonsai.core.tool.Spatial): target_obj.matrix_world = relative_to_obj.matrix_world @ matrix @classmethod - def select_products(cls, products: list[ifcopenshell.entity_instance], unhide: bool = False) -> None: + def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None: bpy.ops.object.select_all(action="DESELECT") for product in products: obj = tool.Ifc.get_object(product) @@ -206,7 +207,7 @@ class Spatial(bonsai.core.tool.Spatial): container = tool.Ifc.get().by_id(container.ifc_definition_id) - results = {} + results: defaultdict[str, dict[int, Any]] = defaultdict(dict) if props.should_include_children: elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True) else: @@ -218,29 +219,47 @@ class Spatial(bonsai.core.tool.Spatial): ifc_class = element.is_a() ifc_definition_id = element_type.id() if element_type else 0 type_name = element_type.Name or "Unnamed" if element_type else f"Untyped {element.is_a()}" - results.setdefault(ifc_class, {}).setdefault(ifc_definition_id, {"total": 0, "type_name": type_name}) - results[ifc_class][ifc_definition_id]["total"] += 1 + class_data = results.setdefault(ifc_class, {}) + type_data = class_data.setdefault(ifc_definition_id, {"type_name": type_name, "elements": []}) + type_data["elements"].append(element) - contracted_classes = json.loads(props.contracted_classes) + expanded_elements = json.loads(props.expanded_elements) + expanded_classes = expanded_elements.get("CLASS", []) + expanded_ifc_ids = expanded_elements.get("IFC_ID", []) + expanded_untyped = expanded_elements.get("UNTYPED_CLASSES", []) total_elements = 0 for ifc_class in sorted(results.keys()): new = props.elements.add() new.name = ifc_class - new.is_class = True - class_is_expanded = ifc_class not in contracted_classes + new.type = "CLASS" + class_is_expanded = ifc_class in expanded_classes new.is_expanded = class_is_expanded total = 0 for ifc_definition_id in sorted( results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"] ): - total2 = results[ifc_class][ifc_definition_id]["total"] + type_data = results[ifc_class][ifc_definition_id] + total2 = len(type_data["elements"]) if class_is_expanded: new2 = props.elements.add() - new2.is_type = True - new2.name = results[ifc_class][ifc_definition_id]["type_name"] + new2.type = "TYPE" + new2.name = type_data["type_name"] new2.ifc_class = ifc_class new2.total = total2 new2.ifc_definition_id = ifc_definition_id + if ifc_definition_id == 0: + type_is_expanded = ifc_class in expanded_untyped + else: + type_is_expanded = ifc_definition_id in expanded_ifc_ids + new2.is_expanded = type_is_expanded + + if type_is_expanded: + for element in type_data["elements"]: + occurrence = props.elements.add() + occurrence.name = element.Name or "Unnamed" + occurrence.ifc_definition_id = element.id() + occurrence.type = "OCCURRENCE" + total += total2 new.total = total total_elements += total @@ -253,12 +272,12 @@ class Spatial(bonsai.core.tool.Spatial): elements: List[ifcopenshell.entity_instance], ifc_class: str | None, relating_type: ifcopenshell.entity_instance | None, - is_untyped: bool | None, + is_untyped: bool, keyword: str | None, ) -> filter[ifcopenshell.entity_instance]: keyword = keyword.lower() if keyword else keyword - def filter_element(element): + def filter_element(element: ifcopenshell.entity_instance) -> bool: if ifc_class: if not element.is_a(ifc_class): return False @@ -344,14 +363,26 @@ class Spatial(bonsai.core.tool.Spatial): props.contracted_containers = json.dumps(contracted_containers) @classmethod - def toggle_container_element(cls, ifc_class: str) -> None: + def toggle_container_element(cls, element_index: int) -> None: props = bpy.context.scene.BIMSpatialDecompositionProperties - contracted_classes: list[str] = json.loads(props.contracted_classes) - if ifc_class in contracted_classes: - contracted_classes.remove(ifc_class) + expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements) + element = props.elements[element_index] + if element.type == "CLASS": + element_type = "CLASS" + filtered_item = element.name else: - contracted_classes.append(ifc_class) - props.contracted_classes = json.dumps(contracted_classes) + if element.ifc_definition_id == 0: + element_type = "UNTYPED_CLASSES" + filtered_item = element.ifc_class + else: + element_type = "IFC_ID" + filtered_item = element.ifc_definition_id + expanded_elements_list: list[Union[str, int]] = expanded_elements.setdefault(element_type, []) + if filtered_item in expanded_elements_list: + expanded_elements_list.remove(filtered_item) + else: + expanded_elements_list.append(filtered_item) + props.expanded_elements = json.dumps(expanded_elements) # HERE STARTS SPATIAL TOOL