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
This commit is contained in:
Andrej730
2025-02-14 17:12:03 +05:00
parent 20e1fc2618
commit 3e1d1bf7fa
6 changed files with 235 additions and 114 deletions
@@ -66,6 +66,7 @@ classes = (
operator.UnlinkIfc,
operator.UnloadLink,
workspace.ExploreHotkey,
prop.LibraryBreadcrumb,
prop.LibraryElement,
prop.FilterCategory,
prop.Link,
+5 -4
View File
@@ -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)):
+105 -38
View File
@@ -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):
+50 -24
View File
@@ -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 = [
+9 -6
View File
@@ -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:
+65 -42
View File
@@ -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()
)