From 3e1d1bf7fa1fc4e2e4f17b6158e4044e780dbf5b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Feb 2025 17:12:03 +0500 Subject: [PATCH] Library UI - tree view Example of tree view with libraries hierarchy - https://imgur.com/a/zPuaoPv Example of library assignment in non-tree view - https://imgur.com/a/9kZFblj --- .../bonsai/bim/module/project/__init__.py | 1 + src/bonsai/bonsai/bim/module/project/data.py | 9 +- .../bonsai/bim/module/project/operator.py | 143 +++++++++++++----- src/bonsai/bonsai/bim/module/project/prop.py | 74 ++++++--- src/bonsai/bonsai/bim/module/project/ui.py | 15 +- src/bonsai/bonsai/tool/project.py | 107 ++++++++----- 6 files changed, 235 insertions(+), 114 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index d87814c026..6cc33bb7b9 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -66,6 +66,7 @@ classes = ( operator.UnlinkIfc, operator.UnloadLink, workspace.ExploreHotkey, + prop.LibraryBreadcrumb, prop.LibraryElement, prop.FilterCategory, prop.Link, diff --git a/src/bonsai/bonsai/bim/module/project/data.py b/src/bonsai/bonsai/bim/module/project/data.py index fe9d5a8d47..50aad50e07 100644 --- a/src/bonsai/bonsai/bim/module/project/data.py +++ b/src/bonsai/bonsai/bim/module/project/data.py @@ -134,10 +134,11 @@ class ProjectLibraryData: @classmethod def project_libraries_enum(cls) -> list[tuple[str, str, str, str, int]]: - results = [ - ("*", "All Libraries", "Show all elements", "", 0), - ("-", "No Library", "Show elements without library assigned", "", 1), - ] + results = [] + project_libraries = cls.data["project_libraries"].values() + if not project_libraries: + results.append(("-", "No Library", "", "", 0)) + props = tool.Project.get_project_props() libs = [] for i, data in enumerate(cls.data["project_libraries"].values(), len(results)): diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 9dcfc7ac60..d4b864ec62 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -60,9 +60,10 @@ from bpy.app.handlers import persistent from ifcopenshell.geom import ShapeElementType from bonsai.bim.module.project.data import LinksData, ProjectLibraryData from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator, MeasureDecorator +from bonsai.bim.module.project.prop import BreadcrumbType from bonsai.bim.module.model.decorator import PolylineDecorator from bonsai.bim.module.model.polyline import PolylineOperator -from typing import Union, TYPE_CHECKING +from typing import Union, TYPE_CHECKING, Literal, get_args class NewProject(bpy.types.Operator): @@ -184,6 +185,7 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector): context.area.tag_redraw() if self.append_all: bpy.ops.bim.append_entire_library() + ProjectLibraryData.load() return {"FINISHED"} def invoke(self, context, event): @@ -218,16 +220,27 @@ class RefreshLibrary(bpy.types.Operator): self.props.library_elements.clear() self.props.library_breadcrumb.clear() - self.props.active_library_element = "" - library_file = IfcStore.library_file assert library_file - condition = tool.Project.get_filter_for_active_library() - for importable_type in sorted(tool.Project.get_appendable_asset_types()): - if (elements := library_file.by_type(importable_type)) and (elements := list(condition(elements))): - elements = self.props.add_library_asset_group(importable_type, len(elements)) + if not self.props.show_library_tree: + for appendable_type in sorted(tool.Project.get_appendable_asset_types()): + elements = library_file.by_type(appendable_type) + self.props.add_library_asset_class(appendable_type, len(elements)) + return {"FINISHED"} + # Library tree. + # Add entry for unassigned elements. + elements = set() + for importable_type in sorted(tool.Project.get_appendable_asset_types()): + elements.update(library_file.by_type(importable_type)) + rels = tool.Project.get_project_library_rels(library_file) + elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)} + self.props.add_library_project_library("Unassigned", len(elements), 0) + + ifc_project = library_file.by_type("IfcProject")[0] + hierarchy = tool.Project.get_project_hierarchy(library_file) + tool.Project.load_project_libraries_to_ui(ifc_project, hierarchy) return {"FINISHED"} @@ -235,10 +248,14 @@ class ChangeLibraryElement(bpy.types.Operator): bl_idname = "bim.change_library_element" bl_label = "Change Library Element" bl_options = {"REGISTER", "UNDO"} - element_name: bpy.props.StringProperty(description="IFC class to select") + element_name: bpy.props.StringProperty() + breadcrumb_type: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(BreadcrumbType)]) + library_id: bpy.props.IntProperty() if TYPE_CHECKING: element_name: str + breadcrumb_type: BreadcrumbType + library_id: int def execute(self, context): self.props = tool.Project.get_project_props() @@ -246,33 +263,81 @@ class ChangeLibraryElement(bpy.types.Operator): library_file = IfcStore.library_file assert library_file self.library_file = library_file - self.props.active_library_element = self.element_name crumb = self.props.library_breadcrumb.add() crumb.name = self.element_name + crumb.breadcrumb_type = self.breadcrumb_type + if self.breadcrumb_type == "LIBRARY": + crumb.library_id = self.library_id - filter_elements = tool.Project.get_filter_for_active_library() - elements = self.library_file.by_type(self.element_name) - elements = list(filter_elements(elements)) - ifc_classes_elements: dict[str, list[ifcopenshell.entity_instance]] = defaultdict(list) - for element in elements: - ifc_classes_elements[element.is_a()].append(element) + active_project_library = None + library_elements = None + project_library_rels = None + # Reverse to get last library in hierarchy. + for entry in reversed(self.props.library_breadcrumb): + if entry.breadcrumb_type == "LIBRARY": + if entry.library_id == 0: + # For unassigned elements. + active_project_library = "NO_LIBRARY" + project_library_rels = tool.Project.get_project_library_rels(library_file) + else: + active_project_library = library_file.by_id(entry.library_id) + library_elements = tool.Project.get_project_library_elements(active_project_library) + break + + def filter_elements(elements: list[ifcopenshell.entity_instance]) -> list[ifcopenshell.entity_instance]: + if active_project_library is None: + return elements + elif active_project_library == "NO_LIBRARY": + assert project_library_rels is not None + return [ + element + for element in elements + if not tool.Project.is_element_assigned_to_project_library(element, project_library_rels) + ] + else: + assert library_elements is not None + return [e for e in elements if e in library_elements] self.props.library_elements.clear() - if len(ifc_classes_elements) == 1 and list(ifc_classes_elements)[0] == self.element_name: - for name, ifc_definition_id in sorted( - [(self.get_name(e), e.id()) for e in ifc_classes_elements[self.element_name]] - ): - self.add_library_asset(name, ifc_definition_id) - else: - for ifc_class in sorted(ifc_classes_elements): - if ifc_class == self.element_name: - continue - self.props.add_library_asset_group(ifc_class, len(ifc_classes_elements[ifc_class])) - elements_ = ifc_classes_elements[self.element_name] - for name, ifc_definition_id, ifc_class in sorted([(self.get_name(e), e.id(), e.is_a()) for e in elements_]): - self.add_library_asset(name, ifc_definition_id) + if self.breadcrumb_type == "LIBRARY": + hierarchy = tool.Project.get_project_hierarchy(library_file) + assert active_project_library is not None + if active_project_library == "NO_LIBRARY" or not hierarchy[active_project_library]: + for appendable_type in sorted(tool.Project.get_appendable_asset_types()): + elements = library_file.by_type(appendable_type) + if elements := filter_elements(elements): + self.props.add_library_asset_class(appendable_type, len(elements)) + else: + tool.Project.load_project_libraries_to_ui(active_project_library, hierarchy) + else: # breadcrumb_type CLASS. + elements = self.library_file.by_type(self.element_name) + elements = list(filter_elements(elements)) + ifc_classes_elements: dict[str, list[ifcopenshell.entity_instance]] = defaultdict(list) + for element in elements: + ifc_classes_elements[element.is_a()].append(element) + + if len(ifc_classes_elements) == 1 and list(ifc_classes_elements)[0] == self.element_name: + for name, ifc_definition_id in sorted( + [(self.get_name(e), e.id()) for e in ifc_classes_elements[self.element_name]] + ): + self.add_library_asset(name, ifc_definition_id) + else: + for ifc_class in sorted(ifc_classes_elements): + if ifc_class == self.element_name: + continue + self.props.add_library_asset_class(ifc_class, len(ifc_classes_elements[ifc_class])) + elements_ = ifc_classes_elements[self.element_name] + for name, ifc_definition_id, ifc_class in sorted( + [(self.get_name(e), e.id(), e.is_a()) for e in elements_] + ): + self.add_library_asset(name, ifc_definition_id) + + # Could occur if all elements were assigned to a different library. + if len(self.props.library_elements) == 0: + bpy.ops.bim.rewind_library() + return {"FINISHED"} def get_name(self, element: ifcopenshell.entity_instance) -> str: @@ -300,10 +365,7 @@ class ChangeLibraryElement(bpy.types.Operator): elif has_context := element.HasContext: relating_context: ifcopenshell.entity_instance relating_context = has_context[0].RelatingContext - if selected_library in ("-", "*"): - new.is_declared = relating_context.is_a("IfcProjectLibrary") - else: - new.is_declared = relating_context == self.library_file.by_id(int(selected_library)) + new.is_declared = relating_context == self.library_file.by_id(int(selected_library)) # is_appended. try: @@ -329,10 +391,17 @@ class RewindLibrary(bpy.types.Operator): if total_breadcrumbs < 2: bpy.ops.bim.refresh_library() return {"FINISHED"} - element_name = self.props.library_breadcrumb[total_breadcrumbs - 2].name + current_element = self.props.library_breadcrumb[total_breadcrumbs - 2] + element_name = current_element.name + breadcrumb_type = current_element.breadcrumb_type + library_id = current_element.library_id self.props.library_breadcrumb.remove(total_breadcrumbs - 1) self.props.library_breadcrumb.remove(total_breadcrumbs - 2) - bpy.ops.bim.change_library_element(element_name=element_name) + bpy.ops.bim.change_library_element( + element_name=element_name, + breadcrumb_type=breadcrumb_type, + library_id=library_id, + ) return {"FINISHED"} @@ -360,10 +429,7 @@ class AssignLibraryDeclaration(bpy.types.Operator): library_file = IfcStore.library_file assert library_file - if props.selected_project_library in ("*", "-"): - project_library = library_file.by_type("IfcProjectLibrary")[0] - else: - project_library = library_file.by_id(int(props.selected_project_library)) + project_library = library_file.by_id(int(props.selected_project_library)) ifcopenshell.api.project.assign_declaration( library_file, @@ -648,6 +714,7 @@ class EditProjectLibrary(bpy.types.Operator): ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library) props.is_editing_project_library = False + bpy.ops.bim.refresh_library() return {"FINISHED"} def rollback(self, data): diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index fe23355846..c94de56614 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -35,7 +35,7 @@ from bpy.props import ( IntProperty, StringProperty, ) -from typing import TYPE_CHECKING, Literal, Union +from typing import TYPE_CHECKING, Literal, Union, get_args def get_export_schema(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -67,15 +67,15 @@ def update_library_file(self: "BIMProjectProperties", context: bpy.types.Context bpy.ops.bim.select_library_file(filepath=filepath.__str__()) ProjectLibraryData.load() props = tool.Project.get_project_props() - props.selected_project_library = "*" + library_file = IfcStore.library_file + assert library_file + project_library = next(iter(library_file.by_type("IfcProjectLibrary")), None) + props.selected_project_library = str(project_library.id()) if project_library else "-" def update_selected_project_library(self: "BIMProjectProperties", context: bpy.types.Context) -> None: - if self.filter_by_library: - bpy.ops.bim.refresh_library() - else: - # Ensure `.is_declared` up to date. - tool.Project.update_current_library_page() + # Ensure `.is_declared` up to date. + tool.Project.update_current_library_page() def get_project_libaries(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -84,10 +84,7 @@ def get_project_libaries(self: "BIMProjectProperties", context: bpy.types.Contex return ProjectLibraryData.data["project_libraries_enum"] -def filter_by_library_update(self: "BIMProjectProperties", context: bpy.types.Context) -> None: - if self.filter_by_library and self.selected_project_library == "*": - # Filter is toggled from OFF to ON, so it was showing all elements previously either way. - return +def show_library_tree_update(self: "BIMProjectProperties", context: bpy.types.Context) -> None: bpy.ops.bim.refresh_library() @@ -150,8 +147,12 @@ def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) new.total_elements = len(ifcopenshell.util.element.get_types(ifc_type)) +LibraryElementType = Literal["ASSET", "CLASS", "LIBRARY"] + + class LibraryElement(PropertyGroup): name: StringProperty(name="Name") + element_type: EnumProperty(items=[(i, i, "") for i in get_args(LibraryElementType)], name="Element Type") # Asset group. asset_count: IntProperty(name="Asset Count") # Asset. @@ -166,6 +167,7 @@ class LibraryElement(PropertyGroup): if TYPE_CHECKING: name: str + element_type: LibraryElementType asset_count: int ifc_definition_id: int is_declared: bool @@ -217,6 +219,18 @@ class EditedObj(PropertyGroup): obj: Union[bpy.types.Object, None] +BreadcrumbType = Literal["LIBRARY", "CLASS"] + + +class LibraryBreadcrumb(PropertyGroup): + breadcrumb_type: EnumProperty(items=[(i, i, "") for i in get_args(BreadcrumbType)]) + library_id: IntProperty(description="IFC Definition ID for libraries.") + + if TYPE_CHECKING: + breadcrumb_type: BreadcrumbType + library_id: int + + class BIMProjectProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing", default=False) is_loading: BoolProperty(name="Is Loading", default=False) @@ -226,8 +240,7 @@ class BIMProjectProperties(PropertyGroup): organisation_name: StringProperty(name="Organisation") organisation_email: StringProperty(name="Organisation Email") authorisation: StringProperty(name="Authoriser") - active_library_element: StringProperty(name="Enable Authoring Mode", default="") - library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty) + library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=LibraryBreadcrumb) library_elements: CollectionProperty(name="Library Elements", type=LibraryElement) active_library_element_index: IntProperty(name="Active Library Element Index") filter_mode: bpy.props.EnumProperty( @@ -321,15 +334,15 @@ class BIMProjectProperties(PropertyGroup): library_file: EnumProperty(items=get_library_file, name="Library File", update=update_library_file) selected_project_library: EnumProperty( items=get_project_libaries, - name="Project Library", - description="Project library to display elements from", + name="Selected Project Library", + description="Selected project library to edit or to assign elements to", update=update_selected_project_library, ) - filter_by_library: BoolProperty( - name="Filter by Library", - description="Filter library elements based on selected library. If unselected can be used to assign selected library to library elements.", + show_library_tree: BoolProperty( + name="Show Library Tree", + description="Show project libraries hierarchy or just show the assets classes.", default=True, - update=filter_by_library_update, + update=show_library_tree_update, ) is_editing_project_library: BoolProperty( name="Is Editing Project Library", @@ -357,10 +370,19 @@ class BIMProjectProperties(PropertyGroup): def clipping_planes_objs(self) -> list[bpy.types.Object]: return list({cp.obj for cp in self.clipping_planes if cp.obj}) - def add_library_asset_group(self, name: str, asset_count: int) -> LibraryElement: + def add_library_project_library(self, name: str, asset_count: int, ifc_definition_id: int) -> LibraryElement: new = self.library_elements.add() new.name = name new.asset_count = asset_count + new.element_type = "LIBRARY" + new.ifc_definition_id = ifc_definition_id + return new + + def add_library_asset_class(self, name: str, asset_count: int) -> LibraryElement: + new = self.library_elements.add() + new.name = name + new.asset_count = asset_count + new.element_type = "CLASS" return new def get_library_element_index(self, lib_element: LibraryElement) -> int: @@ -375,8 +397,7 @@ class BIMProjectProperties(PropertyGroup): organisation_name: str organisation_email: str authorisation: str - active_library_element: str - library_breadcrumb: bpy.types.bpy_prop_collection_idprop[StrProperty] + library_breadcrumb: bpy.types.bpy_prop_collection_idprop[LibraryBreadcrumb] library_elements: bpy.types.bpy_prop_collection_idprop[LibraryElement] active_library_element_index: int filter_mode: Literal["NONE", "DECOMPOSITION", "IFC_CLASS", "IFC_TYPE", "WHITELIST", "BLACKLIST"] @@ -410,8 +431,8 @@ class BIMProjectProperties(PropertyGroup): template_file: str library_file: str - selected_project_library: Union[Literal["*", "-"], str] - filter_by_library: bool + selected_project_library: Union[Literal["-"], str] + show_library_tree: bool is_editing_project_library: bool editing_project_library_id: int project_library_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] @@ -424,6 +445,11 @@ class BIMProjectProperties(PropertyGroup): clipping_planes_active: int edited_objs: bpy.types.bpy_prop_collection_idprop[EditedObj] + def get_active_library_breadcrumb(self) -> Union[LibraryBreadcrumb, None]: + if self.library_breadcrumb: + return self.library_breadcrumb[-1] + return None + class MeasureToolSettings(PropertyGroup): measurement_type_items = [ diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 51505ed46e..361f2d1a61 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -392,7 +392,7 @@ class BIM_PT_project_library(Panel): library_file = IfcStore.library_file assert library_file - library_is_selected = props.selected_project_library not in ("*", "-") + library_is_selected = props.selected_project_library != "-" row = layout.row(align=True) row.prop(self.props, "selected_project_library", text="") @@ -403,7 +403,7 @@ class BIM_PT_project_library(Panel): if library_is_selected and not props.is_editing_project_library: row.prop(props, "is_editing_project_library", text="", icon="GREASEPENCIL") - row.prop(self.props, "filter_by_library", text="", icon="FILTER") + row.prop(self.props, "show_library_tree", text="", icon="OUTLINER") if props.is_editing_project_library: row = layout.row(align=True) @@ -420,8 +420,9 @@ class BIM_PT_project_library(Panel): return row = self.layout.row(align=True) - row.label(text=self.props.active_library_element or "Top Level Assets") - if self.props.active_library_element: + active_library_element = self.props.get_active_library_breadcrumb() + row.label(text=(active_library_element.name if active_library_element else "Top Level Assets")) + if active_library_element: row.operator("bim.rewind_library", icon="FRAME_PREV", text="") row.operator("bim.refresh_library", icon="FILE_REFRESH", text="") self.layout.template_list( @@ -511,9 +512,11 @@ class BIM_UL_library(UIList): ): if item: row = layout.row(align=True) - if not item.ifc_definition_id: + if item.element_type != "ASSET" and item.asset_count > 0: op = row.operator("bim.change_library_element", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) op.element_name = item.name + op.breadcrumb_type = item.element_type + op.library_id = item.ifc_definition_id row.label(text=item.name) if item.ifc_definition_id and item.is_declarable: if item.is_declared: @@ -522,7 +525,7 @@ class BIM_UL_library(UIList): else: op = row.operator("bim.assign_library_declaration", text="", icon="KEYFRAME", emboss=False) op.definition = item.ifc_definition_id - if item.ifc_definition_id: + if item.element_type == "ASSET": if item.is_appended: row.label(text="", icon="CHECKMARK") else: diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 96a5c58c36..ae966b49de 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -31,6 +31,7 @@ import bonsai.core.unit import bonsai.core.owner import bonsai.bim.schema import bonsai.tool as tool +from collections import defaultdict from bonsai.bim.ifc import IfcStore from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES from pathlib import Path @@ -39,6 +40,8 @@ from typing import Optional, Union, TYPE_CHECKING, Generator, Callable if TYPE_CHECKING: from bonsai.bim.module.project.prop import BIMProjectProperties +HiearchyDict = dict[ifcopenshell.entity_instance, "HiearchyDict"] + class Project(bonsai.core.tool.Project): @classmethod @@ -296,54 +299,43 @@ class Project(bonsai.core.tool.Project): ifcopenshell.api.document.remove_reference(ifc_file, reference) @classmethod - def get_filter_for_active_library( + def get_project_library_elements( + cls, project_library: ifcopenshell.entity_instance + ) -> set[ifcopenshell.entity_instance]: + return set(element for rel in project_library.Declares for element in rel.RelatedDefinitions) + + @classmethod + def get_project_library_rels(cls, ifc_file: ifcopenshell.file) -> set[ifcopenshell.entity_instance]: + return set(rel for lib in ifc_file.by_type("IfcProjectLibrary") for rel in lib.Declares) + + @classmethod + def is_element_assigned_to_project_library( cls, - ) -> Callable[[list[ifcopenshell.entity_instance]], Generator[ifcopenshell.entity_instance, None, None]]: - props = cls.get_project_props() - library_file = IfcStore.library_file - assert library_file - - selected_project_library = props.selected_project_library if props.filter_by_library else "*" - - if selected_project_library == "*": - - def condition(elements: list[ifcopenshell.entity_instance]): - yield from elements - - elif selected_project_library == "-": - - def condition(elements: list[ifcopenshell.entity_instance]): - for element in elements: - if not getattr(element, "HasContext", False): - yield element - - else: - project_library = library_file.by_id(int(selected_project_library)) - project_library_rels = set(project_library.Declares) - if project_library_rels: - - def condition(elements: list[ifcopenshell.entity_instance]): - for element in elements: - for rel in getattr(element, "HasContext", ()): - if rel in project_library_rels: - yield element - break - - else: - - def condition(elements: list[ifcopenshell.entity_instance]): - # Hacky way to create empty generator. - return - yield - - return condition + element: ifcopenshell.entity_instance, + project_library_rels: set[ifcopenshell.entity_instance], + ) -> bool: + if not (has_context := getattr(element, "HasContext", ())): + return False + return any(rel in project_library_rels for rel in has_context) @classmethod def update_current_library_page(cls): props = cls.get_project_props() - element_name = props.active_library_element + active_library_breadcrumb = props.get_active_library_breadcrumb() + change_back = False + if active_library_breadcrumb: + name = active_library_breadcrumb.name + breadcrumb_type = active_library_breadcrumb.breadcrumb_type + library_id = active_library_breadcrumb.library_id + change_back = True + bpy.ops.bim.rewind_library() - bpy.ops.bim.change_library_element(element_name=element_name) + if change_back: + bpy.ops.bim.change_library_element( + element_name=name, + breadcrumb_type=breadcrumb_type, + library_id=library_id, + ) @classmethod def get_parent_library(cls, project_library: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: @@ -352,3 +344,34 @@ class Project(bonsai.core.tool.Project): return nests[0].RelatingObject # IfcProject. return project_library.HasContext[0].RelatingContext + + @classmethod + def get_project_hierarchy(cls, ifc_file: ifcopenshell.file) -> HiearchyDict: + """Get project hierarchy in the following form: + + { + IfcProject: { IfcProjectLibrary A: { ... }, }, + IfcProjectLibrary A: { IfcProjectLibrary B: { ... } }, + IfcProjectLibrary B: { ... }, + } + + Use IfcProject to get hierarchy root. + + """ + hierarchy: HiearchyDict = defaultdict(dict) + for project_library in ifc_file.by_type("IfcProjectLibrary"): + parent_library = cls.get_parent_library(project_library) + hierarchy[parent_library][project_library] = hierarchy[project_library] + return hierarchy + + @classmethod + def load_project_libraries_to_ui( + cls, parent_library: ifcopenshell.entity_instance, hierarchy: HiearchyDict + ) -> None: + libraries = hierarchy[parent_library] + props = cls.get_project_props() + for project_library in libraries: + library_elements = tool.Project.get_project_library_elements(project_library) + props.add_library_project_library( + project_library.Name or "Unnamed", len(library_elements), project_library.id() + )