From 47b81a6f894b2cceb56cbb067061042a2f4afbdf Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 4 Jul 2025 14:16:32 +0500 Subject: [PATCH] bim.save_search - prefer only SEARCH IfcGroups by default #6857 Previously it was asking for a name to use to save search and users might type anything they find convenient at the moment, but it was possible that this name was used already by something important (for example drawing's IfcGroup) and would change it's description and assign objects there unexpectedly. Now it will target IfcGroups with SEARCH ObjectType by default and only if user confirms, it will target all IfcGroups available (to keep the search a powerful tool for advanced users that allows them to populate any IfcGroup they want with items). There's also a temporary patch in place - when you load/save search Bonsai will try to find old search IfcGroups with ObjectType = None and update their ObjectType, this needed to make sure transition works seamlessly for the simple cases. --- src/bonsai/bonsai/bim/module/search/data.py | 8 ++- .../bonsai/bim/module/search/operator.py | 67 +++++++++++++++++-- src/bonsai/bonsai/tool/search.py | 23 +++++++ 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/search/data.py b/src/bonsai/bonsai/bim/module/search/data.py index c82f26e7d6..34f032c2b5 100644 --- a/src/bonsai/bonsai/bim/module/search/data.py +++ b/src/bonsai/bonsai/bim/module/search/data.py @@ -20,6 +20,7 @@ import bpy import json import ifcopenshell.util.element import bonsai.tool as tool +from natsort import natsorted def refresh(): @@ -39,11 +40,11 @@ class SearchData: cls.data["saved_searches"] = cls.saved_searches() @classmethod - def saved_searches(cls): + def saved_searches(cls) -> list[tuple[str, str, str]]: if not tool.Ifc.get(): return [] groups = tool.Ifc.get().by_type("IfcGroup") - results = [] + results: list[ifcopenshell.entity_instance] = [] for group in groups: try: data = json.loads(group.Description) @@ -51,7 +52,8 @@ class SearchData: results.append(group) except: pass - return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")] + enum_items = [(str(g.id()), g.Name or "Unnamed", "") for g in results] + return natsorted(enum_items, key=lambda x: x[1]) class ColourByPropertyData: diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 223c80baa9..8ae7d52376 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -179,10 +179,49 @@ class Search(Operator): class SaveSearch(Operator, tool.Ifc.Operator): bl_idname = "bim.save_search" bl_label = "Save Search" - bl_description = "Save search filter to an IFC group" + bl_description = ( + "Save search filter to an IFC group.\n\n" + "Search query will be saved to group description, query elements will be assigned to the group." + ) bl_options = {"REGISTER", "UNDO"} - name: StringProperty(name="Name") - module: StringProperty() + + name_search_items: list[str] = [] + + def get_name_search_items(self, context: object, text: str) -> list[str]: + # Extra item so it will be easy to select current text. + return [text] + SaveSearch.name_search_items + + name: StringProperty( # pyright: ignore[reportRedeclaration] + name="Name", + search=get_name_search_items, + search_options={"SORT"}, + ) + module: StringProperty() # pyright: ignore[reportRedeclaration] + + def update_use_all_ifcgroups(self, context: object = None) -> None: + ifc_file = tool.Ifc.get() + groups = { + g.Name or "Unnamed" + for g in ifc_file.by_type("IfcGroup") + if self.use_all_ifcgroups or g.ObjectType == "SEARCH" + } + self.name_search_items[:] = natsorted(groups) + + use_all_ifcgroups: BoolProperty( # pyright: ignore[reportRedeclaration] + name="Use Any IfcGroup", + description=( + "By default we're targeting only IfcGroups with SEARCH ObjectType " + "to prevent breaking internal IfcGroups (e.g. IfcGroups used for Bonsai drawings).\n\n" + "Enabling this option allows saving search to any IfcGroup matched by the provided name.\n" + "Use with caution." + ), + update=update_use_all_ifcgroups, + ) + + if TYPE_CHECKING: + name: str + module: str + use_all_ifcgroups: bool def _execute(self, context): if not self.name: @@ -199,20 +238,32 @@ class SaveSearch(Operator, tool.Ifc.Operator): return description = json.dumps({"type": "BBIM_Search", "query": query}) - group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name] + ifc_file = tool.Ifc.get() + group = next( + ( + g + for g in ifc_file.by_type("IfcGroup") + if g.Name == self.name and (self.use_all_ifcgroups or g.ObjectType == "SEARCH") + ), + None, + ) if group: - group = group[0] group.Description = description else: group = ifcopenshell.api.group.add_group(tool.Ifc.get(), name=self.name, description=description) + group.ObjectType = "SEARCH" if results: ifcopenshell.api.group.assign_group(tool.Ifc.get(), products=list(results), group=group) def draw(self, context): - row = self.layout.row() - row.prop(self, "name") + assert (layout := self.layout) + layout.prop(self, "name") + layout.prop(self, "use_all_ifcgroups") def invoke(self, context, event): + assert context.window_manager + tool.Search.patch_search_ifcgroups() + self.update_use_all_ifcgroups() return context.window_manager.invoke_props_dialog(self) @@ -230,11 +281,13 @@ class LoadSearch(Operator, tool.Ifc.Operator): tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) def draw(self, context): + assert self.layout props = tool.Search.get_search_props() row = self.layout.row() row.prop(props, "saved_searches", text="") def invoke(self, context, event): + tool.Search.patch_search_ifcgroups() return context.window_manager.invoke_props_dialog(self) diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index ce805a3bf3..b304fe7b81 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -311,6 +311,29 @@ class Search(bonsai.core.tool.Search): query = f"bpy.data.texts['{name}']" return query + @classmethod + def patch_search_ifcgroups(cls) -> None: + """Apply a patch trying to convert old search IfcGroups to SEARCH type. + + Previously we were saving search results to IfcGroup with ObjectType None + and allowing to write results to any IfcGroup by default, which could lead + to breaking by accident drawings or other internal IfcGroups. + + Added temporarily @25.07.04 + """ + ifc_file = tool.Ifc.get() + + for group in ifc_file.by_type("IfcGroup"): + # It's unsafe to change any other IfcGroup - e.g. it could be DRAWING. + if group.ObjectType is not None: + continue + try: + data = json.loads(group.Description) + if isinstance(data, dict) and data.get("type") == "BBIM_Search": + group.ObjectType = "SEARCH" + except: + pass + class ImportFilterQueryTransformer(lark.Transformer): def __init__(self, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]):