diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 76f539afc8..8f46411b6e 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -64,6 +64,10 @@ def update_should_include_children(self, context): tool.Spatial.load_contained_elements() +def update_element_mode(self, context): + tool.Spatial.load_contained_elements() + + def update_container_obj(self, context): if self.container_obj is None or not (obj := context.active_object): return @@ -112,6 +116,8 @@ class Element(PropertyGroup): 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", ) + level: IntProperty(name="Level", default=0) + has_children: BoolProperty(name="Has Children", default=False) total: IntProperty(name="Total") is_expanded: BoolProperty(name="Is Expanded", default=False) type: EnumProperty( @@ -134,6 +140,14 @@ class BIMSpatialDecompositionProperties(PropertyGroup): expanded_elements: StringProperty(name="Expanded Elements", default="{}") active_element_index: IntProperty(name="Active Element Index") total_elements: IntProperty(name="Total Elements") + element_mode: EnumProperty( + name="Element Mode", + items=( + ("TYPE", "Class > Type > Occurrence", "Show a breakdown by IfcClass, relating type, and occurrence"), + ("DECOMPOSITION", "Element Decomposition", "Show a breakdown by element decomposition"), + ), + update=update_element_mode, + ) subelement_class: bpy.props.EnumProperty(items=get_subelement_class, name="Subelement Class") default_container: IntProperty(name="Default Container", default=0) should_include_children: BoolProperty( diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index 33a5d87c07..6ee712d4dd 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -160,8 +160,11 @@ class BIM_PT_spatial_decomposition(Panel): if not self.props.total_elements: row = self.layout.row(align=True) row.label(text=f"{self.props.active_container.ifc_class} > No Elements", icon="FILE_3D") - row.prop(self.props, "should_include_children", text="", icon="OUTLINER") row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id + + row = self.layout.row(align=True) + row.prop(self.props, "element_mode", text="", icon="FILEBROWSER") + row.prop(self.props, "should_include_children", text="", icon="OUTLINER") return row = self.layout.row(align=True) @@ -169,7 +172,6 @@ class BIM_PT_spatial_decomposition(Panel): text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements", icon="FILE_3D", ) - row.prop(self.props, "should_include_children", text="", icon="OUTLINER") row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id op = row.operator("bim.select_decomposed_element", icon="OBJECT_DATA", text="") if active_element := self.props.active_element: @@ -179,6 +181,10 @@ class BIM_PT_spatial_decomposition(Panel): op = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="") op.container = ifc_definition_id + row = self.layout.row(align=True) + row.prop(self.props, "element_mode", text="", icon="FILEBROWSER") + row.prop(self.props, "should_include_children", text="", icon="OUTLINER") + self.layout.template_list( "BIM_UL_elements", "", @@ -273,23 +279,26 @@ class BIM_UL_elements(UIList): if item: row = layout.row(align=True) item_type = item.type - if item_type == "CLASS": + 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) col = row.column() col.alignment = "RIGHT" col.label(text=str(item.total)) 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") + 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/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index d9a6a75a10..8f23f71bd3 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -226,6 +226,14 @@ class Spatial(bonsai.core.tool.Spatial): return container = tool.Ifc.get().by_id(container.ifc_definition_id) + if props.element_mode == "TYPE": + cls.load_contained_elements_by_type(container) + elif props.element_mode == "DECOMPOSITION": + cls.load_contained_elements_by_decomposition(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) expanded_elements = json.loads(props.expanded_elements) @@ -239,6 +247,7 @@ class Spatial(bonsai.core.tool.Spatial): new = props.elements.add() new.name = ifc_class new.type = "CLASS" + new.has_children = True class_is_expanded = ifc_class in expanded_classes class_is_expanded_r = ifc_class in expanded_classes_r new.is_expanded = class_is_expanded or class_is_expanded_r @@ -251,6 +260,8 @@ class Spatial(bonsai.core.tool.Spatial): if new.is_expanded: new2 = props.elements.add() new2.type = "TYPE" + new2.has_children = True + new2.level = 1 new2.name = type_data["type_name"] new2.ifc_class = ifc_class new2.total = total2 @@ -265,6 +276,7 @@ class Spatial(bonsai.core.tool.Spatial): for element in type_data["elements"]: occurrence = props.elements.add() occurrence.name = element.Name or "Unnamed" + occurrence.level = 2 occurrence.ifc_definition_id = element.id() occurrence.type = "OCCURRENCE" @@ -274,6 +286,35 @@ class Spatial(bonsai.core.tool.Spatial): props.total_elements = total_elements + @classmethod + def load_contained_elements_by_decomposition(cls, container: ifcopenshell.entity_instance) -> None: + props = bpy.context.scene.BIMSpatialDecompositionProperties + expanded_elements = json.loads(props.expanded_elements) + expanded_ifc_ids = expanded_elements.get("IFC_ID", []) + + 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.level = level + new.ifc_definition_id = ifc_definition_id + new.type = "OCCURRENCE" + children = [ + e + for e in ifcopenshell.util.element.get_decomposition(element, is_recursive=False) + if not e.is_a("IfcFeatureElement") + ] + if children: + new.has_children = True + new.total = len(children) + if ifc_definition_id in expanded_ifc_ids: + add_elements(children, level=level + 1) + + elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=False) + add_elements(elements) + @classmethod def filter_elements( cls, @@ -373,7 +414,14 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def toggle_container_element(cls, element_index: int, is_recursive: bool) -> None: props = bpy.context.scene.BIMSpatialDecompositionProperties + if props.element_mode == "TYPE": + 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) + @classmethod + def toggle_container_element_by_type(cls, element_index: int, is_recursive: bool) -> None: + props = bpy.context.scene.BIMSpatialDecompositionProperties expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements) element = props.elements[element_index] @@ -420,6 +468,39 @@ class Spatial(bonsai.core.tool.Spatial): props.expanded_elements = json.dumps(expanded_elements) + @classmethod + def toggle_container_element_by_decomposition(cls, element_index: int, is_recursive: bool) -> None: + props = bpy.context.scene.BIMSpatialDecompositionProperties + element = props.elements[element_index] + expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements) + expanded_elements_list: list[Union[str, int]] = expanded_elements.setdefault("IFC_ID", []) + ifc_definition_id = element.ifc_definition_id + if ifc_definition_id in expanded_elements_list: + expanded_elements_list.remove(ifc_definition_id) + should_expand = False + else: + expanded_elements_list.append(ifc_definition_id) + should_expand = True + + if is_recursive: + queue = [tool.Ifc.get().by_id(ifc_definition_id)] + while queue: + element = queue.pop() + children = [ + e + for e in ifcopenshell.util.element.get_decomposition(element, is_recursive=False) + if not e.is_a("IfcFeatureElement") + ] + ifc_definition_id = element.id() + if children: + if should_expand is False and ifc_definition_id in expanded_elements_list: + expanded_elements_list.remove(ifc_definition_id) + elif should_expand is True and ifc_definition_id not in expanded_elements_list: + expanded_elements_list.append(ifc_definition_id) + queue.extend(children) + + props.expanded_elements = json.dumps(expanded_elements) + # HERE STARTS SPATIAL TOOL @classmethod