From ce35903459d66414eaed4f5c0c46a90a4821b031 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Tue, 16 Dec 2025 17:13:07 +0100 Subject: [PATCH 1/9] First commit for a simplified searching operation with suggestions --- src/bonsai/bonsai/bim/helper.py | 218 ++++++- .../bonsai/bim/module/search/__init__.py | 3 + .../bonsai/bim/module/search/operator.py | 559 +++++++++++++++++- src/bonsai/bonsai/bim/prop.py | 10 + src/bonsai/bonsai/bim/ui.py | 8 + src/bonsai/bonsai/tool/search.py | 212 +++++-- 6 files changed, 953 insertions(+), 57 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 49f49582b4..a59e9964f6 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -489,6 +489,8 @@ def draw_filter( data.load() sprops = tool.Search.get_search_props() + preferences = tool.Blender.get_addon_preferences() + enable_suggestions = getattr(preferences, "search_filter_suggestions", False) if tool.Ifc.get(): row = layout.row(align=True) @@ -498,6 +500,8 @@ def draw_filter( if data.data["saved_searches"]: row.operator("bim.load_search", text="", icon="IMPORT").module = module row.operator("bim.save_search", text="", icon="EXPORT").module = module + if data.data["saved_searches"] and enable_suggestions: + row.operator("bim.remove_search", text="", icon="REMOVE").module = module if module != "search": if module == "drawing_include": row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "INCLUDE" @@ -505,7 +509,15 @@ def draw_filter( row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE" row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE" row = layout.row(align=True) - row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module + if not enable_suggestions: + row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module + else: + # When suggestions are enabled, show a simple "Add Filter" button if no filters exist + if not filter_groups or not any(fg.filters for fg in filter_groups): + op = row.operator("bim.add_filter", text="Add Filter", icon="ADD") + op.type = "entity" + op.index = 0 + op.module = module row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module for i, filter_group in enumerate(filter_groups): @@ -524,43 +536,247 @@ def draw_filter( for j, ifc_filter in enumerate(filter_group.filters): if ifc_filter.type == "entity": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "value", text="", icon="FILE_3D") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "attribute": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "name", text="", icon="COPY_ID") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type + op.suggestion_type = "attribute_name" row.prop(ifc_filter, "value", text="") + if enable_suggestions and ifc_filter.name: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type + op.suggestion_type = "attribute_value" elif ifc_filter.type == "type": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "value", text="", icon="FILE_VOLUME") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "material": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "value", text="", icon="MATERIAL") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "property": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "pset", text="", icon="PROPERTIES") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type + op.suggestion_type = "pset" row.prop(ifc_filter, "name", text="") + if enable_suggestions and ifc_filter.pset: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type + op.suggestion_type = "property_name" row.prop(ifc_filter, "comparison", text="") row.prop(ifc_filter, "value", text="") + if enable_suggestions and ifc_filter.pset and ifc_filter.name: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type + op.suggestion_type = "property_value" elif ifc_filter.type == "classification": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "value", text="", icon="OUTLINER") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "location": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "value", text="", icon="PACKAGE") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "group": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "value", text="", icon="OUTLINER_COLLECTION") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "parent": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "value", text="", icon="FILE_PARENT") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "query": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA") row.prop(ifc_filter, "comparison", text="") row.prop(ifc_filter, "value", text="") elif ifc_filter.type == "instance": row = box.row(align=True) + if enable_suggestions and j > 0: + mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} + op = row.operator( + "bim.toggle_filter_inclusion", + icon=mode_icons.get(ifc_filter.filter_mode, "ADD"), + text="", + depress=ifc_filter.filter_mode != "ADD", + ) + op.group_index = i + op.filter_index = j + op.module = module row.prop(ifc_filter, "value", text="", icon="GRIP") + if enable_suggestions: + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type op = row.operator("bim.select_filter_elements", text="", icon="EYEDROPPER") op.group_index = i op.index = j diff --git a/src/bonsai/bonsai/bim/module/search/__init__.py b/src/bonsai/bonsai/bim/module/search/__init__.py index e1849c63ae..bc3951cc83 100644 --- a/src/bonsai/bonsai/bim/module/search/__init__.py +++ b/src/bonsai/bonsai/bim/module/search/__init__.py @@ -26,10 +26,12 @@ classes = ( operator.AddFilterGroup, operator.ColourByProperty, operator.EditFilterQuery, + operator.FilterValueSuggestions, operator.LoadColourscheme, operator.LoadSearch, operator.RemoveFilter, operator.RemoveFilterGroup, + operator.RemoveSearch, operator.ResetObjectColours, operator.SaveColourscheme, operator.SaveSearch, @@ -40,6 +42,7 @@ classes = ( operator.SelectIfcClass, operator.SelectSimilar, operator.ShowAllElements, + operator.ToggleFilterInclusion, operator.ToggleFilterSelection, prop.BIMColour, prop.BIMFilterItem, diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 1fb164047b..9ffe2f25e0 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -40,9 +40,465 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from bonsai.bim.prop import StrProperty from typing import TYPE_CHECKING, Literal, get_args, assert_never +def update_filter_search_value(self: "FilterValueSuggestions", context: bpy.types.Context) -> None: + filter_groups = tool.Search.get_filter_groups(self.module) + ifc_filter = filter_groups[self.group_index].filters[self.filter_index] + + value = self.search_value + + if " < " in value: + value = value.split(" < ")[-1] + + if ifc_filter.type == "entity": + if " (superclass)" in value: + value = value.replace(" (superclass)", "") + if " > " in value: + value = value.split(" > ")[-1] + + elif ifc_filter.type == "instance": + if ": " in value: + value = value.split(": ")[-1] + elif " (" in value: + hierarchy_class = value.split(" (") + element_class = hierarchy_class[-1].rstrip(")") + element_name = hierarchy_class[0] if len(hierarchy_class) > 1 else None + + ifc_file = tool.Ifc.get() + if ifc_file: + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + if element.is_a() == element_class: + if element_name is None or (hasattr(element, 'Name') and element.Name == element_name): + value = element.GlobalId + break + except: + continue + + elif ifc_filter.type == "parent": + if " (" in value: + value = value.split(" (")[0] + + if ifc_filter.type == "property": + if self.suggestion_type == "pset": + ifc_filter.pset = value + elif self.suggestion_type == "property_name": + ifc_filter.name = value + else: + ifc_filter.value = value + elif ifc_filter.type == "attribute": + if self.suggestion_type == "attribute_name": + ifc_filter.name = value + else: + ifc_filter.value = value + else: + ifc_filter.value = value + + if self.first_launch: + self.first_launch = False + else: + context.window.screen = context.window.screen + + +class FilterValueSuggestions(Operator): + bl_idname = "bim.filter_value_suggestions" + bl_label = "Filter Value Suggestions" + bl_description = "Get suggestions for filter values from the current IFC file" + bl_options = {"REGISTER", "UNDO"} + + group_index: IntProperty() + filter_index: IntProperty() + module: StringProperty() + filter_type: StringProperty() + suggestion_type: StringProperty(default="value") + + first_launch: BoolProperty(default=True, options={"SKIP_SAVE"}) + search_value: StringProperty( + name="Search", + description="Search for filter values", + update=update_filter_search_value, + default="", + options={"SKIP_SAVE"}, + ) + collection_values: CollectionProperty(type=StrProperty, options={"SKIP_SAVE"}) + + def execute(self, context): + return {"FINISHED"} + + def invoke(self, context, event): + ifc_file = tool.Ifc.get() + if not ifc_file: + self.report({"WARNING"}, "No IFC file loaded") + return {"CANCELLED"} + + filter_groups = tool.Search.get_filter_groups(self.module) + ifc_filter = filter_groups[self.group_index].filters[self.filter_index] + + string_suggestions = self.get_suggestions(ifc_file, ifc_filter) + if not string_suggestions: + self.report({"INFO"}, f"No suggestions available") + return {"CANCELLED"} + + self.collection_values.clear() + for suggestion in natsorted(string_suggestions): + self.collection_values.add().name = suggestion + + return context.window_manager.invoke_props_dialog(self, width=300) + + def draw(self, context): + layout = self.layout + + filter_groups = tool.Search.get_filter_groups(self.module) + ifc_filter = filter_groups[self.group_index].filters[self.filter_index] + + label_map = { + "entity": "Select Class", + "type": "Select Type", + "material": "Select Material", + "location": "Select Location", + "group": "Select Group", + "classification": "Select Classification", + "parent": "Select Parent", + "instance": "Select GlobalId", + } + + if ifc_filter.type == "attribute": + if self.suggestion_type == "attribute_value": + label = f"Select Value for {ifc_filter.name}" + else: + label = "Select Attribute Name" + elif ifc_filter.type == "property": + if self.suggestion_type == "property_value": + label = f"Select Value for {ifc_filter.pset}.{ifc_filter.name}" + elif self.suggestion_type == "property_name": + label = f"Select Property from {ifc_filter.pset}" + else: + label = "Select Property Set" + else: + label = label_map.get(ifc_filter.type, "Select Value") + + row = layout.row() + row.label(text=label) + row = layout.row() + row.prop_search( + self, + "search_value", + self, + "collection_values", + text="", + results_are_suggestions=True, + ) + + def get_suggestions(self, ifc_file, ifc_filter): + suggestions = set() + + if ifc_filter.type == "entity": + suggestions = self.get_entity_suggestions(ifc_file) + elif ifc_filter.type == "type": + suggestions = self.get_type_suggestions(ifc_file) + elif ifc_filter.type == "material": + suggestions = self.get_material_suggestions(ifc_file) + elif ifc_filter.type == "location": + suggestions = self.get_location_suggestions(ifc_file) + elif ifc_filter.type == "group": + suggestions = self.get_group_suggestions(ifc_file) + elif ifc_filter.type == "classification": + suggestions = self.get_classification_suggestions(ifc_file) + elif ifc_filter.type == "parent": + suggestions = self.get_parent_suggestions(ifc_file) + elif ifc_filter.type == "instance": + suggestions = self.get_instance_suggestions(ifc_file) + elif ifc_filter.type == "attribute": + if self.suggestion_type == "attribute_name": + suggestions = self.get_attribute_names(ifc_file) + else: + suggestions = self.get_attribute_values(ifc_file, ifc_filter.name) + elif ifc_filter.type == "property": + if self.suggestion_type == "pset": + suggestions = self.get_property_sets(ifc_file) + elif self.suggestion_type == "property_name": + suggestions = self.get_property_names(ifc_file, ifc_filter.pset) + else: + suggestions = self.get_property_values(ifc_file, ifc_filter.pset, ifc_filter.name) + + return suggestions + + def build_hierarchy_path(self, element): + path = [] + current = element + + while current: + if hasattr(current, 'Name') and current.Name: + path.insert(0, current.Name) + + parent = None + + if hasattr(current, 'Decomposes') and current.Decomposes: + for rel in current.Decomposes: + if hasattr(rel, 'RelatingObject'): + parent = rel.RelatingObject + break + + if not parent and hasattr(current, 'ContainedInStructure') and current.ContainedInStructure: + for rel in current.ContainedInStructure: + if hasattr(rel, 'RelatingStructure'): + parent = rel.RelatingStructure + break + + current = parent + + return path + + def get_entity_suggestions(self, ifc_file): + all_classes = set() + schema = tool.Ifc.schema() + + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + class_name = element.is_a() + + try: + entity = schema.declaration_by_name(class_name).as_entity() + current = entity + + chain_names = [class_name] + while current.supertype(): + supertype = current.supertype() + chain_names.insert(0, supertype.name()) + current = supertype + + if len(chain_names) > 1: + all_classes.add(" > ".join(chain_names)) + for i in range(len(chain_names) - 1): + superclass_chain = " > ".join(chain_names[:i+1]) + all_classes.add(f"{superclass_chain} (superclass)") + else: + all_classes.add(class_name) + except: + all_classes.add(class_name) + except: + continue + + return all_classes + + def get_type_suggestions(self, ifc_file): + suggestions = set() + for element_type in ifc_file.by_type("IfcTypeObject"): + if element_type.Name: + hierarchy_path = self.build_hierarchy_path(element_type) + if len(hierarchy_path) > 1: + suggestions.add(" < ".join(hierarchy_path)) + else: + suggestions.add(element_type.Name) + return suggestions + + def get_material_suggestions(self, ifc_file): + suggestions = set() + for material in ifc_file.by_type("IfcMaterial"): + if material.Name: + suggestions.add(material.Name) + return suggestions + + def get_location_suggestions(self, ifc_file): + suggestions = set() + for spatial in ifc_file.by_type("IfcSpatialStructureElement"): + if spatial.Name: + hierarchy_path = self.build_hierarchy_path(spatial) + if len(hierarchy_path) > 1: + suggestions.add(" < ".join(hierarchy_path)) + else: + suggestions.add(spatial.Name) + return suggestions + + def get_group_suggestions(self, ifc_file): + suggestions = set() + for group in ifc_file.by_type("IfcGroup"): + if group.Name: + hierarchy_path = self.build_hierarchy_path(group) + if len(hierarchy_path) > 1: + suggestions.add(" < ".join(hierarchy_path)) + else: + suggestions.add(group.Name) + return suggestions + + def get_classification_suggestions(self, ifc_file): + suggestions = set() + for ref in ifc_file.by_type("IfcClassificationReference"): + if ref.Identification: + suggestions.add(ref.Identification) + return suggestions + + def get_parent_suggestions(self, ifc_file): + suggestions = set() + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + has_children = False + + if hasattr(element, 'IsDecomposedBy') and element.IsDecomposedBy: + has_children = True + elif hasattr(element, 'ContainsElements') and element.ContainsElements: + has_children = True + elif hasattr(element, 'HasOpenings') and element.HasOpenings: + has_children = True + + if has_children: + hierarchy_path = self.build_hierarchy_path(element) + element_class = element.is_a() + + if len(hierarchy_path) > 0: + hierarchy_str = " < ".join(hierarchy_path) + suggestions.add(f"{hierarchy_str} ({element_class})") + else: + element_name = element.Name if hasattr(element, 'Name') and element.Name else element_class + suggestions.add(f"{element_name} ({element_class})") + except: + continue + + return suggestions + + def get_instance_suggestions(self, ifc_file): + suggestions = set() + element_data = [] + + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + if hasattr(element, 'GlobalId') and element.GlobalId: + hierarchy_path = self.build_hierarchy_path(element) + element_class = element.is_a() + + if len(hierarchy_path) > 0: + hierarchy_str = " < ".join(hierarchy_path) + display_str = f"{hierarchy_str} ({element_class})" + else: + display_str = f"({element_class})" + + element_data.append((display_str, element.GlobalId)) + except: + continue + + display_counts = {} + for display_str, global_id in element_data: + display_counts[display_str] = display_counts.get(display_str, 0) + 1 + + for display_str, global_id in element_data: + if display_counts[display_str] > 1: + suggestions.add(f"{display_str}: {global_id}") + else: + suggestions.add(display_str) + + return suggestions + + def get_property_sets(self, ifc_file): + psets = set() + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + for definition in getattr(element, 'IsDefinedBy', []): + if definition.is_a('IfcRelDefinesByProperties'): + pset = definition.RelatingPropertyDefinition + if pset.is_a("IfcPropertySet") and pset.Name: + psets.add(pset.Name) + except: + continue + return psets + + def get_property_names(self, ifc_file, pset_name): + property_names = set() + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + for definition in getattr(element, 'IsDefinedBy', []): + if definition.is_a('IfcRelDefinesByProperties'): + pset = definition.RelatingPropertyDefinition + if pset.is_a("IfcPropertySet") and pset.Name == pset_name: + if pset.HasProperties: + for prop in pset.HasProperties: + if hasattr(prop, "Name") and prop.Name: + property_names.add(prop.Name) + except: + continue + return property_names + + def get_property_values(self, ifc_file, pset_name, property_name): + property_values = set() + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + for definition in getattr(element, 'IsDefinedBy', []): + if definition.is_a('IfcRelDefinesByProperties'): + pset = definition.RelatingPropertyDefinition + if pset.is_a("IfcPropertySet") and pset.Name == pset_name: + if pset.HasProperties: + for prop in pset.HasProperties: + if hasattr(prop, "Name") and prop.Name == property_name: + if hasattr(prop, "NominalValue") and prop.NominalValue: + try: + value = prop.NominalValue.wrappedValue + if value is not None and value != "": + if not hasattr(value, 'is_a') and not isinstance(value, (tuple, list)): + str_value = str(value) + if not str_value.startswith("#") and not str_value.startswith("("): + property_values.add(str_value) + except: + continue + except: + continue + return property_values + + def get_attribute_names(self, ifc_file): + attribute_names = set() + schema = tool.Ifc.schema() + + ifc_classes = set() + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + ifc_classes.add(element.is_a()) + except: + continue + + for ifc_class in ifc_classes: + try: + entity = schema.declaration_by_name(ifc_class).as_entity() + attributes = entity.all_attributes() + for attr in attributes: + attribute_names.add(attr.name()) + except: + continue + + return attribute_names + + def get_attribute_values(self, ifc_file, attribute_name): + attribute_values = set() + + for element_id in IfcStore.id_map.keys(): + try: + element = ifc_file.by_id(element_id) + + if element.is_a("IfcRelationship") or element.is_a("IfcTypeObject"): + continue + + if hasattr(element, attribute_name): + value = getattr(element, attribute_name, None) + + if value is not None and value != "": + if not hasattr(value, 'is_a') and not isinstance(value, (tuple, list)): + str_value = str(value) + if not str_value.startswith("#") and not str_value.startswith("("): + attribute_values.add(str_value) + except: + continue + + return attribute_values + + class AddFilterGroup(Operator): bl_idname = "bim.add_filter_group" bl_label = "Add Filter Group" @@ -96,11 +552,38 @@ class AddFilter(Operator): def execute(self, context): filter_groups = tool.Search.get_filter_groups(self.module) + if self.index >= len(filter_groups): + filter_groups.add() new = filter_groups[self.index].filters.add() new.type = self.type return {"FINISHED"} +class ToggleFilterInclusion(Operator): + bl_idname = "bim.toggle_filter_inclusion" + bl_label = "Toggle Filter Mode" + bl_description = "Cycle between Add (+), Subtract (-), and Filter modes for this filter" + bl_options = {"REGISTER", "UNDO"} + + group_index: IntProperty() + filter_index: IntProperty() + module: StringProperty() + + def execute(self, context): + filter_groups = tool.Search.get_filter_groups(self.module) + filter_group = filter_groups[self.group_index] + ifc_filter = filter_group.filters[self.filter_index] + + if ifc_filter.filter_mode == "ADD": + ifc_filter.filter_mode = "SUBTRACT" + elif ifc_filter.filter_mode == "SUBTRACT": + ifc_filter.filter_mode = "FILTER" + else: + ifc_filter.filter_mode = "ADD" + + return {"FINISHED"} + + class SelectFilterElements(bpy.types.Operator): bl_idname = "bim.select_filter_elements" bl_label = "Select Filter Elements" @@ -174,9 +657,15 @@ class Search(Operator): else: assert_never(self.property_group) - results = ifcopenshell.util.selector.filter_elements( - tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups) - ) + preferences = tool.Blender.get_addon_preferences() + enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + + if enable_suggestions: + results = tool.Search.execute_filter_groups(props.filter_groups) + else: + results = ifcopenshell.util.selector.filter_elements( + tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups) + ) objs = [obj for e in results if isinstance(obj := tool.Ifc.get_object(e), bpy.types.Object)] for obj in objs: @@ -242,13 +731,32 @@ class SaveSearch(Operator, tool.Ifc.Operator): try: query = tool.Search.export_filter_query(filter_groups) - results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + results = tool.Search.execute_filter_groups(filter_groups) + + filter_structure = [] + for filter_group in filter_groups: + group_data = [] + for ifc_filter in filter_group.filters: + filter_data = { + "type": ifc_filter.type, + "name": ifc_filter.name, + "value": ifc_filter.value, + "pset": ifc_filter.pset, + "comparison": ifc_filter.comparison, + "filter_mode": ifc_filter.filter_mode, + } + group_data.append(filter_data) + filter_structure.append(group_data) except: print(traceback.format_exc()) self.report({"ERROR"}, "Error occurred trying save search.") return - description = json.dumps({"type": "BBIM_Search", "query": query}) + description = json.dumps({ + "type": "BBIM_Search", + "query": query, + "filter_structure": filter_structure + }) ifc_file = tool.Ifc.get() group = next( ( @@ -289,7 +797,12 @@ class LoadSearch(Operator, tool.Ifc.Operator): filter_groups = tool.Search.get_filter_groups(self.module) props = tool.Search.get_search_props() group = tool.Ifc.get().by_id(int(props.saved_searches)) - tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) + + group_data = tool.Search.get_group_data(group) + if group_data and "filter_structure" in group_data: + tool.Search.import_filter_structure(group_data["filter_structure"], filter_groups) + else: + tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) def draw(self, context): assert self.layout @@ -302,6 +815,40 @@ class LoadSearch(Operator, tool.Ifc.Operator): return context.window_manager.invoke_props_dialog(self) +class RemoveSearch(Operator, tool.Ifc.Operator): + bl_idname = "bim.remove_search" + bl_label = "Remove Search" + bl_description = "Remove a saved search filter" + bl_options = {"REGISTER", "UNDO"} + module: StringProperty() + + def _execute(self, context): + props = tool.Search.get_search_props() + group_id = props.saved_searches + if not group_id: + self.report({"ERROR"}, "No search selected for removal") + return + + group = tool.Ifc.get().by_id(int(group_id)) + group_name = group.Name or "Unnamed" + ifcopenshell.api.group.remove_group(tool.Ifc.get(), group=group) + tool.Search.patch_search_ifcgroups() + self.report({"INFO"}, f"Removed saved search: {group_name}") + + def draw(self, context): + self.layout.label(text="Select search to remove:", icon="ERROR") + row = self.layout.row() + props = tool.Search.get_search_props() + row.prop(props, "saved_searches", text="") + + def invoke(self, context, event): + tool.Search.patch_search_ifcgroups() + from bonsai.bim.module.search.data import SearchData + if not SearchData.is_loaded: + SearchData.load() + return context.window_manager.invoke_props_dialog(self) + + class ColourByProperty(Operator): bl_idname = "bim.colour_by_property" bl_label = "Colour by Property" diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 08137a133e..3ca57b27cc 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -748,6 +748,15 @@ class BIMFacet(PropertyGroup): pset: StringProperty(name="Pset") value: StringProperty(name="Value") type: StringProperty(name="Type") + filter_mode: EnumProperty( + name="Filter Mode", + items=[ + ("ADD", "Add", "Add elements to the result set (query entire IFC file)", "ADD", 0), + ("SUBTRACT", "Subtract", "Subtract matching elements from previous results", "REMOVE", 1), + ("FILTER", "Filter", "Filter down previous results to matching elements", "FILTER", 2), + ], + default="ADD", + ) comparison: EnumProperty( items=[ ("=", "equal to", ""), @@ -765,6 +774,7 @@ class BIMFacet(PropertyGroup): pset: str value: str type: str + filter_mode: Literal["ADD", "SUBTRACT", "FILTER"] comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="] diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index d41546904c..d3cd1012fc 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -689,6 +689,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): description="Show mass and time units section in the new project wizard panel", default=False, ) + + search_filter_suggestions: BoolProperty( + name="Enable advanced search with filter suggestions", + description="Enable filter mode (ADD/SUBTRACT/FILTER) and value suggestions for search filters", + default=False, + ) if TYPE_CHECKING: svg2pdf_command: str @@ -727,6 +733,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): default_parameters: DefaultParameters container_hide_show_isolate: bool mass_time_units_in_wizard: bool + search_filter_suggestions: bool def draw(self, context: bpy.types.Context) -> None: layout = self.layout @@ -909,6 +916,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "container_hide_show_isolate") layout.prop(self, "mass_time_units_in_wizard") + layout.prop(self, "search_filter_suggestions") # Scene panel groups diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index 29e862b6b0..bcd2424c71 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -42,6 +42,38 @@ class Search(bonsai.core.tool.Search): def get_group_query(cls, group: ifcopenshell.entity_instance) -> str: return json.loads(group.Description)["query"] + @classmethod + def get_group_data(cls, group: ifcopenshell.entity_instance) -> dict: + return json.loads(group.Description) + + @classmethod + def import_filter_structure( + cls, filter_structure: list, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + ) -> None: + filter_groups.clear() + + for group_data in filter_structure: + if not isinstance(group_data, list): + continue + + filter_group = filter_groups.add() + + for filter_data in group_data: + if not isinstance(filter_data, dict): + continue + + ifc_filter = filter_group.filters.add() + ifc_filter.type = filter_data.get("type", "") + ifc_filter.name = filter_data.get("name", "") + ifc_filter.value = filter_data.get("value", "") + ifc_filter.pset = filter_data.get("pset", "") + ifc_filter.comparison = filter_data.get("comparison", "=") + filter_mode = filter_data.get("filter_mode", "ADD") + if filter_mode in ["ADD", "SUBTRACT", "FILTER"]: + ifc_filter.filter_mode = filter_mode + else: + ifc_filter.filter_mode = "ADD" + FilterModule = Union[Literal["search", "csv", "diff", "drawing_include", "drawing_exclude"], str] @classmethod @@ -80,53 +112,127 @@ class Search(bonsai.core.tool.Search): for ifc_filter in filter_group.filters: if not ifc_filter.value: continue - if ifc_filter.type == "instance": - if "bpy.data.texts" in ifc_filter.value: - data_name = ifc_filter.value.split("bpy.data.texts")[1][2:-2] - filter_group_query.append(bpy.data.texts[data_name].as_string()) - else: - filter_group_query.append(ifc_filter.value) - elif ifc_filter.type == "entity": - filter_group_query.append(ifc_filter.value) - elif ifc_filter.type == "attribute": - if not ifc_filter.name: - continue - comparison, value = cls.get_comparison_and_value(ifc_filter) - filter_group_query.append(f"{ifc_filter.name}{comparison}{value}") - elif ifc_filter.type == "type": - comparison, value = cls.get_comparison_and_value(ifc_filter) - filter_group_query.append(f"type{comparison}{value}") - elif ifc_filter.type == "material": - comparison, value = cls.get_comparison_and_value(ifc_filter) - filter_group_query.append(f"material{comparison}{value}") - elif ifc_filter.type == "property": - if not ifc_filter.pset or not ifc_filter.name: - continue - pset = cls.wrap_value(ifc_filter, ifc_filter.pset) - name = cls.wrap_value(ifc_filter, ifc_filter.name) - comparison = ifc_filter.comparison - value = cls.wrap_value(ifc_filter, ifc_filter.value) - filter_group_query.append(f"{pset}.{name} {comparison} {value}") - elif ifc_filter.type == "classification": - comparison, value = cls.get_comparison_and_value(ifc_filter) - filter_group_query.append(f"classification{comparison}{value}") - elif ifc_filter.type == "location": - comparison, value = cls.get_comparison_and_value(ifc_filter) - filter_group_query.append(f"location{comparison}{value}") - elif ifc_filter.type == "group": - comparison, value = cls.get_comparison_and_value(ifc_filter) - filter_group_query.append(f"group{comparison}{value}") - elif ifc_filter.type == "parent": - comparison, value = cls.get_comparison_and_value(ifc_filter) - filter_group_query.append(f"parent{comparison}{value}") - elif ifc_filter.type == "query": - keys = cls.wrap_value(ifc_filter, ifc_filter.name) - comparison = ifc_filter.comparison or "=" - value = cls.wrap_value(ifc_filter, ifc_filter.value) - filter_group_query.append(f"query:{keys}{comparison}{value}") - query.append(", ".join(filter_group_query)) + + query_part = cls._export_single_filter(ifc_filter) + if query_part: + filter_group_query.append(query_part) + + if filter_group_query: + query.append(", ".join(filter_group_query)) return " + ".join(query) + @classmethod + def _export_single_filter(cls, ifc_filter: BIMFacet) -> str: + if ifc_filter.type == "instance": + if "bpy.data.texts" in ifc_filter.value: + data_name = ifc_filter.value.split("bpy.data.texts")[1][2:-2] + value = bpy.data.texts[data_name].as_string() + else: + value = ifc_filter.value + value = value.lstrip("!") + if ifc_filter.filter_mode == "SUBTRACT": + value = f"!{value}" + return value + elif ifc_filter.type == "entity": + value = ifc_filter.value + value = value.lstrip("!") + if ifc_filter.filter_mode == "SUBTRACT": + value = f"!{value}" + return value + elif ifc_filter.type == "attribute": + if not ifc_filter.name: + return "" + comparison, value = cls.get_comparison_and_value(ifc_filter) + return f"{ifc_filter.name}{comparison}{value}" + elif ifc_filter.type == "type": + comparison, value = cls.get_comparison_and_value(ifc_filter) + return f"type{comparison}{value}" + elif ifc_filter.type == "material": + comparison, value = cls.get_comparison_and_value(ifc_filter) + return f"material{comparison}{value}" + elif ifc_filter.type == "property": + if not ifc_filter.pset or not ifc_filter.name: + return "" + pset = cls.wrap_value(ifc_filter, ifc_filter.pset) + name = cls.wrap_value(ifc_filter, ifc_filter.name) + comparison = ifc_filter.comparison + value = cls.wrap_value(ifc_filter, ifc_filter.value) + return f"{pset}.{name} {comparison} {value}" + elif ifc_filter.type == "classification": + comparison, value = cls.get_comparison_and_value(ifc_filter) + return f"classification{comparison}{value}" + elif ifc_filter.type == "location": + comparison, value = cls.get_comparison_and_value(ifc_filter) + return f"location{comparison}{value}" + elif ifc_filter.type == "group": + comparison, value = cls.get_comparison_and_value(ifc_filter) + return f"group{comparison}{value}" + elif ifc_filter.type == "parent": + comparison, value = cls.get_comparison_and_value(ifc_filter) + return f"parent{comparison}{value}" + elif ifc_filter.type == "query": + keys = cls.wrap_value(ifc_filter, ifc_filter.name) + comparison = ifc_filter.comparison or "=" + value = cls.wrap_value(ifc_filter, ifc_filter.value) + return f"query:{keys}{comparison}{value}" + return "" + + @classmethod + def execute_filter_groups(cls, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]) -> set: + """ + Execute filter groups with simplified chaining support. + Within a single group chain, all filters chain sequentially with ADD/SUBTRACT/FILTER modes. + Groups are combined with union (same as original " + " behavior). + """ + all_group_results = [] + + for filter_group in filter_groups: + group_results = set() + + for filter_index, ifc_filter in enumerate(filter_group.filters): + if not ifc_filter.value: + continue + + query = cls._export_single_filter(ifc_filter) + if not query: + continue + + mode = "ADD" if filter_index == 0 else ifc_filter.filter_mode + + if mode == "ADD": + results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + group_results.update(results) + + elif mode == "SUBTRACT": + if group_results: + query_without_prefix = query[1:] if query.startswith("!") else query + elements_to_remove = ifcopenshell.util.selector.filter_elements( + tool.Ifc.get(), query_without_prefix, elements=group_results + ) + group_results -= elements_to_remove + else: + results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + group_results.update(results) + + elif mode == "FILTER": + if group_results: + results = ifcopenshell.util.selector.filter_elements( + tool.Ifc.get(), query, elements=group_results + ) + group_results = results + else: + results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + group_results.update(results) + + if group_results: + all_group_results.append(group_results) + + final_results = set() + for group_results in all_group_results: + final_results.update(group_results) + + return final_results + @classmethod def get_comparison_and_value( cls, ifc_filter: BIMFacet @@ -349,7 +455,8 @@ class ImportFilterQueryTransformer(lark.Transformer): def facet_list(self, args): new = self.filter_groups.add() global_ids = [] - for arg in args: + is_first_group = len(self.filter_groups) == 1 + for filter_index, arg in enumerate(args): if arg["type"] == "instance" and global_ids: if "bpy.data.texts" in new2.value: data_name = new2.value.split("bpy.data.texts")[1][2:-2] @@ -374,21 +481,26 @@ class ImportFilterQueryTransformer(lark.Transformer): new2.pset = arg["pset"] if "comparison" in arg: new2.comparison = arg["comparison"] or "=" + if "filter_mode" in arg: + new2.filter_mode = arg["filter_mode"] + elif not is_first_group and filter_index == 0 and arg["type"] in ("entity", "instance"): + if arg.get("filter_mode", "ADD") != "SUBTRACT": + new2.filter_mode = "FILTER" def facet(self, args): return args[0] def instance(self, args): if args[0].data == "not": - return {"type": "instance", "value": "!" + args[1].children[0].value} + return {"type": "instance", "value": args[1].children[0].value, "filter_mode": "SUBTRACT"} else: - return {"type": "instance", "value": args[0].children[0].value} + return {"type": "instance", "value": args[0].children[0].value, "filter_mode": "ADD"} def entity(self, args): if args[0].data == "not": - return {"type": "entity", "value": "!" + args[1].children[0].value} + return {"type": "entity", "value": args[1].children[0].value, "filter_mode": "SUBTRACT"} else: - return {"type": "entity", "value": args[0].children[0].value} + return {"type": "entity", "value": args[0].children[0].value, "filter_mode": "ADD"} def attribute(self, args): name, comparison, value = args From bf75509fdf7882535ce20dde361a982057c70341 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Tue, 16 Dec 2025 19:47:41 +0100 Subject: [PATCH 2/9] Enhance filtering capabilities with JSON support for include/exclude operations --- .../bonsai/bim/module/drawing/operator.py | 52 ++++++++-- src/bonsai/bonsai/tool/drawing.py | 39 +++++++- src/bonsai/bonsai/tool/search.py | 96 +++++++++++++++++++ 3 files changed, 177 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 248fae5775..434880d5da 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3661,8 +3661,12 @@ class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator): if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()): filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}") try: - tool.Search.import_filter_query(query, filter_groups) - except: + data = json.loads(query) + if isinstance(data, dict) and "filter_structure" in data: + tool.Search.import_filter_structure(data["filter_structure"], filter_groups) + else: + tool.Search.import_filter_query(query, filter_groups) + except Exception: pass @@ -3682,12 +3686,48 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): assert element pset = tool.Pset.get_element_pset(element, "EPset_Drawing") assert pset + + preferences = tool.Blender.get_addon_preferences() + enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + if self.filter_mode == "INCLUDE": - query = tool.Search.export_filter_query(props.include_filter_groups) or None - ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": query}) + filter_groups = props.include_filter_groups elif self.filter_mode == "EXCLUDE": - query = tool.Search.export_filter_query(props.exclude_filter_groups) or None - ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": query}) + filter_groups = props.exclude_filter_groups + else: + return + + query = tool.Search.export_filter_query(filter_groups) or None + + if enable_suggestions and query: + filter_structure = [] + for filter_group in filter_groups: + group_data = [] + for ifc_filter in filter_group.filters: + filter_data = { + "type": ifc_filter.type, + "name": ifc_filter.name, + "value": ifc_filter.value, + "pset": ifc_filter.pset, + "comparison": ifc_filter.comparison, + "filter_mode": ifc_filter.filter_mode, + } + group_data.append(filter_data) + filter_structure.append(group_data) + + value = json.dumps({ + "type": "BBIM_Search", + "query": query, + "filter_structure": filter_structure + }) + else: + value = query + + if self.filter_mode == "INCLUDE": + ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": value}) + elif self.filter_mode == "EXCLUDE": + ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": value}) + props.filter_mode = "NONE" bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 8fc500a223..0f063b8473 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -2277,7 +2277,16 @@ class Drawing(bonsai.core.tool.Drawing): pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {}) include = pset.get("Include", None) if include: - elements = ifcopenshell.util.selector.filter_elements(ifc_file, include) + try: + data = json.loads(include) + if isinstance(data, dict) and "filter_structure" in data: + elements = tool.Search.execute_filter_groups_from_json(data, ifc_file) + elif isinstance(data, dict) and "query" in data: + elements = ifcopenshell.util.selector.filter_elements(ifc_file, data["query"]) + else: + elements = ifcopenshell.util.selector.filter_elements(ifc_file, include) + except (json.JSONDecodeError, ValueError): + elements = ifcopenshell.util.selector.filter_elements(ifc_file, include) else: if ifc_file.schema == "IFC2X3": base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement")) @@ -2291,7 +2300,7 @@ class Drawing(bonsai.core.tool.Drawing): if not i.is_a("IfcAnnotation"): updated_set.add(i) # add aggregate too, if element is host by one - if decomposes := i.Decomposes: + if hasattr(i, "Decomposes") and (decomposes := i.Decomposes): aggregate = decomposes[0].RelatingObject # remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615 if aggregate.is_a("IfcProduct"): @@ -2304,7 +2313,18 @@ class Drawing(bonsai.core.tool.Drawing): exclude = pset.get("Exclude", None) if exclude: - elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) + try: + data = json.loads(exclude) + if isinstance(data, dict) and "filter_structure" in data: + exclude_elements = tool.Search.execute_filter_groups_from_json(data, ifc_file) + elements -= exclude_elements + elif isinstance(data, dict) and "query" in data: + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, data["query"]) + else: + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) + except (json.JSONDecodeError, ValueError): + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) elements -= set(ifc_file.by_type("IfcOpeningElement")) return elements @@ -2318,7 +2338,18 @@ class Drawing(bonsai.core.tool.Drawing): # NOTE: EPset_Drawing.Include is not used to avoid adding other elements besides spaces exclude = pset.get("Exclude", None) if exclude: - elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) + try: + data = json.loads(exclude) + if isinstance(data, dict) and "filter_structure" in data: + exclude_elements = tool.Search.execute_filter_groups_from_json(data, ifc_file) + elements -= exclude_elements + elif isinstance(data, dict) and "query" in data: + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, data["query"]) + else: + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) + except (json.JSONDecodeError, ValueError): + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) + elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude) return elements @classmethod diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index bcd2424c71..ece6062f49 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -233,6 +233,102 @@ class Search(bonsai.core.tool.Search): return final_results + @classmethod + def execute_filter_groups_from_json( + cls, data: dict, ifc_file: ifcopenshell.file + ) -> set[ifcopenshell.entity_instance]: + """Execute filter groups from JSON data with filter_structure + + This is used by drawing include/exclude to properly handle ADD/SUBTRACT/FILTER modes + without needing to instantiate Blender property groups. + """ + filter_structure = data.get("filter_structure", []) + + all_group_results = [] + for group_data in filter_structure: + group_results = set() + + for filter_data in group_data: + filter_mode = filter_data.get("filter_mode", "ADD") + filter_type = filter_data.get("type", "") + value = filter_data.get("value", "") + + if not filter_type or not value: + continue + + query_part = None + if filter_type == "entity": + query_part = value + elif filter_type == "attribute": + name = filter_data.get("name", "") + if not name: + continue + comparison = filter_data.get("comparison", "=") + query_part = f"{name}{comparison}{cls._wrap_json_value(value)}" + elif filter_type == "property": + pset = filter_data.get("pset", "") + name = filter_data.get("name", "") + if not pset or not name: + continue + comparison = filter_data.get("comparison", " = ") + wrapped_pset = cls._wrap_json_value(pset) + wrapped_name = cls._wrap_json_value(name) + wrapped_value = cls._wrap_json_value(value) + query_part = f"{wrapped_pset}.{wrapped_name} {comparison} {wrapped_value}" + elif filter_type == "type": + query_part = f"type={cls._wrap_json_value(value)}" + elif filter_type == "material": + query_part = f"material={cls._wrap_json_value(value)}" + elif filter_type == "classification": + query_part = f"classification={cls._wrap_json_value(value)}" + elif filter_type == "location": + query_part = f"location={cls._wrap_json_value(value)}" + elif filter_type == "group": + query_part = f"group={cls._wrap_json_value(value)}" + elif filter_type == "parent": + query_part = f"parent={cls._wrap_json_value(value)}" + elif filter_type == "query": + name = filter_data.get("name", "") + if not name: + continue + keys = cls._wrap_json_value(name) + comparison = filter_data.get("comparison", "=") + wrapped_value = cls._wrap_json_value(value) + query_part = f"query:{keys}{comparison}{wrapped_value}" + elif filter_type == "instance": + query_part = value + + if not query_part: + continue + + if filter_mode == "FILTER" and group_results: + results = ifcopenshell.util.selector.filter_elements(ifc_file, query_part, elements=group_results) + group_results = results + elif filter_mode == "SUBTRACT": + results = ifcopenshell.util.selector.filter_elements(ifc_file, query_part) + group_results -= results + else: # ADD + results = ifcopenshell.util.selector.filter_elements(ifc_file, query_part) + group_results.update(results) + + if group_results: + all_group_results.append(group_results) + + final_results = set() + for group_results in all_group_results: + final_results.update(group_results) + + return final_results + + @classmethod + def _wrap_json_value(cls, value: str) -> str: + """Wrap value for use in query string""" + if value.startswith("/") and value.endswith("/"): + return value + elif value in ("NULL", "TRUE", "FALSE"): + return value + return '"' + value.replace('"', '\\"') + '"' + @classmethod def get_comparison_and_value( cls, ifc_filter: BIMFacet From 965a303b4b23ca814b0f8ed1da438f34dfef2ea3 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 17 Dec 2025 00:25:57 +0100 Subject: [PATCH 3/9] Add edit filter query option for backward compatibility in draw_filter function --- src/bonsai/bonsai/bim/helper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index a59e9964f6..c4c9cf0c7d 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -511,14 +511,13 @@ def draw_filter( row = layout.row(align=True) if not enable_suggestions: row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module + row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module else: - # When suggestions are enabled, show a simple "Add Filter" button if no filters exist if not filter_groups or not any(fg.filters for fg in filter_groups): op = row.operator("bim.add_filter", text="Add Filter", icon="ADD") op.type = "entity" op.index = 0 op.module = module - row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module for i, filter_group in enumerate(filter_groups): box = layout.box() From d6e9d7d33915382e7650d63d6f3bf363e81e9f33 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 17 Dec 2025 10:55:14 +0100 Subject: [PATCH 4/9] use edit_filter_query to edit json configuration --- src/bonsai/bonsai/bim/helper.py | 4 +- .../bonsai/bim/module/search/__init__.py | 3 + .../bonsai/bim/module/search/operator.py | 139 ++++++++++++++++-- 3 files changed, 130 insertions(+), 16 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index c4c9cf0c7d..1fac2c9b5f 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -511,13 +511,15 @@ def draw_filter( row = layout.row(align=True) if not enable_suggestions: row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module - row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module else: if not filter_groups or not any(fg.filters for fg in filter_groups): op = row.operator("bim.add_filter", text="Add Filter", icon="ADD") op.type = "entity" op.index = 0 op.module = module + op = row.operator("bim.edit_filter_query", text="", icon="FILTER") + if "module" in op.bl_rna.properties: + op.module = module for i, filter_group in enumerate(filter_groups): box = layout.box() diff --git a/src/bonsai/bonsai/bim/module/search/__init__.py b/src/bonsai/bonsai/bim/module/search/__init__.py index bc3951cc83..88cb2f6ed4 100644 --- a/src/bonsai/bonsai/bim/module/search/__init__.py +++ b/src/bonsai/bonsai/bim/module/search/__init__.py @@ -24,6 +24,7 @@ classes = ( operator.ActivateIfcClassFilter, operator.AddFilter, operator.AddFilterGroup, + operator.ApplyFilterFromText, operator.ColourByProperty, operator.EditFilterQuery, operator.FilterValueSuggestions, @@ -58,7 +59,9 @@ classes = ( def register(): bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties) + bpy.types.TEXT_HT_header.append(operator.draw_text_editor_header) def unregister(): del bpy.types.Scene.BIMSearchProperties + bpy.types.TEXT_HT_header.remove(operator.draw_text_editor_header) diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 9ffe2f25e0..081a0df7ae 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -44,6 +44,13 @@ from bonsai.bim.prop import StrProperty from typing import TYPE_CHECKING, Literal, get_args, assert_never +def draw_text_editor_header(self, context): + if context.space_data.text and context.space_data.text.name.startswith("FilterQuery_"): + layout = self.layout + layout.separator() + op = layout.operator("bim.apply_filter_from_text", text="Apply Filter Configuration", icon="CHECKMARK") + + def update_filter_search_value(self: "FilterValueSuggestions", context: bpy.types.Context) -> None: filter_groups = tool.Search.get_filter_groups(self.module) ifc_filter = filter_groups[self.group_index].filters[self.filter_index] @@ -112,7 +119,7 @@ class FilterValueSuggestions(Operator): group_index: IntProperty() filter_index: IntProperty() - module: StringProperty() + module: StringProperty(default="search") filter_type: StringProperty() suggestion_type: StringProperty(default="value") @@ -603,6 +610,47 @@ class SelectFilterElements(bpy.types.Operator): return {"FINISHED"} +class ApplyFilterFromText(Operator, tool.Ifc.Operator): + bl_idname = "bim.apply_filter_from_text" + bl_label = "Apply Filter Configuration" + bl_description = "Apply the JSON filter configuration from the current text block" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if context.area and context.area.type == 'TEXT_EDITOR': + space = context.space_data + if space.text and space.text.name.startswith("FilterQuery_"): + return True + return False + + def execute(self, context): + space = context.space_data + text = space.text + + if not text or not text.name.startswith("FilterQuery_"): + self.report({"ERROR"}, "No valid filter configuration text block") + return {'CANCELLED'} + + module = text.name.replace("FilterQuery_", "") + + try: + json_data = json.loads(text.as_string()) + filter_structure = json_data.get("filter_structure", []) + filter_groups = tool.Search.get_filter_groups(module) + tool.Search.import_filter_structure(filter_structure, filter_groups) + self.report({"INFO"}, "Filter configuration applied successfully") + + if len(context.window_manager.windows) > 1: + bpy.ops.wm.window_close() + + except Exception as e: + self.report({"ERROR"}, f"Invalid JSON: {str(e)}") + return {'CANCELLED'} + + return {'FINISHED'} + + class EditFilterQuery(Operator, tool.Ifc.Operator): bl_idname = "bim.edit_filter_query" bl_label = "Edit Filter Query" @@ -610,29 +658,90 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} query: StringProperty(name="Query") old_query: StringProperty(name="Old Query") - module: StringProperty() + module: StringProperty(default="search") def _execute(self, context): - if self.query == self.old_query: - return + module = getattr(self, "module", "search") + preferences = tool.Blender.get_addon_preferences() + enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + + if not enable_suggestions: + if self.query == self.old_query: + return - filter_groups = tool.Search.get_filter_groups(self.module) - try: - tool.Search.import_filter_query(self.query, filter_groups) - except: - return + filter_groups = tool.Search.get_filter_groups(module) + try: + tool.Search.import_filter_query(self.query, filter_groups) + except: + return def draw(self, context): - row = self.layout.row() - row.prop(self, "query", text="") + preferences = tool.Blender.get_addon_preferences() + enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + + if not enable_suggestions: + row = self.layout.row() + row.prop(self, "query", text="") def invoke(self, context, event): - filter_groups = tool.Search.get_filter_groups(self.module) + module = getattr(self, "module", "search") + filter_groups = tool.Search.get_filter_groups(module) + preferences = tool.Blender.get_addon_preferences() + enable_suggestions = getattr(preferences, "search_filter_suggestions", False) - self.query = tool.Search.export_filter_query(filter_groups) - self.old_query = self.query + if enable_suggestions: + filter_structure = [] + for filter_group in filter_groups: + group_data = [] + for ifc_filter in filter_group.filters: + filter_data = { + "type": ifc_filter.type, + "name": ifc_filter.name, + "value": ifc_filter.value, + "pset": ifc_filter.pset, + "comparison": ifc_filter.comparison, + "filter_mode": ifc_filter.filter_mode, + } + group_data.append(filter_data) + filter_structure.append(group_data) + + query = tool.Search.export_filter_query(filter_groups) + json_data = { + "type": "BBIM_Search", + "query": query, + "filter_structure": filter_structure + } + + text_block_name = f"FilterQuery_{module}" + text = bpy.data.texts.get(text_block_name) + if not text: + text = bpy.data.texts.new(text_block_name) + + text.clear() + text.write(json.dumps(json_data, indent=2)) + + bpy.ops.wm.window_new() + new_window = context.window_manager.windows[-1] + + new_area = new_window.screen.areas[0] + new_area.type = 'TEXT_EDITOR' + + text_space = None + for space in new_area.spaces: + if space.type == 'TEXT_EDITOR': + text_space = space + break + + if text_space: + text_space.text = text + + self.report({"INFO"}, "Compact text editor opened. Edit JSON and click 'Apply Filter Configuration' in header") + return {'FINISHED'} - return context.window_manager.invoke_props_dialog(self) + else: + self.query = tool.Search.export_filter_query(filter_groups) + self.old_query = self.query + return context.window_manager.invoke_props_dialog(self, width=400) class Search(Operator): From 8912cca65253c9e4f9b31b0a9921560ffa63cbe9 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 17 Dec 2025 11:13:17 +0100 Subject: [PATCH 5/9] Increase dialog width for filter value suggestions to improve usability --- src/bonsai/bonsai/bim/module/search/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 081a0df7ae..fc78ed9aa5 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -154,7 +154,7 @@ class FilterValueSuggestions(Operator): for suggestion in natsorted(string_suggestions): self.collection_values.add().name = suggestion - return context.window_manager.invoke_props_dialog(self, width=300) + return context.window_manager.invoke_props_dialog(self, width=800) def draw(self, context): layout = self.layout From 1477d650a49fea46c3c16bc5f3a5fb669151b9d4 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Wed, 17 Dec 2025 19:28:00 +0100 Subject: [PATCH 6/9] Rename to chain_filter_with_set_operations for clarity and update related preferences --- src/bonsai/bonsai/bim/helper.py | 2 +- src/bonsai/bonsai/bim/module/drawing/operator.py | 2 +- src/bonsai/bonsai/bim/module/search/operator.py | 8 ++++---- src/bonsai/bonsai/bim/ui.py | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 1fac2c9b5f..16c6226326 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -490,7 +490,7 @@ def draw_filter( sprops = tool.Search.get_search_props() preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) if tool.Ifc.get(): row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 434880d5da..76eb3c7afa 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3688,7 +3688,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): assert pset preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) if self.filter_mode == "INCLUDE": filter_groups = props.include_filter_groups diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index fc78ed9aa5..75dccd4d95 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -663,7 +663,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): def _execute(self, context): module = getattr(self, "module", "search") preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) if not enable_suggestions: if self.query == self.old_query: @@ -677,7 +677,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): def draw(self, context): preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) if not enable_suggestions: row = self.layout.row() @@ -687,7 +687,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): module = getattr(self, "module", "search") filter_groups = tool.Search.get_filter_groups(module) preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) if enable_suggestions: filter_structure = [] @@ -767,7 +767,7 @@ class Search(Operator): assert_never(self.property_group) preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "search_filter_suggestions", False) + enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) if enable_suggestions: results = tool.Search.execute_filter_groups(props.filter_groups) diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index d3cd1012fc..eacd6ec472 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -690,9 +690,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): default=False, ) - search_filter_suggestions: BoolProperty( - name="Enable advanced search with filter suggestions", - description="Enable filter mode (ADD/SUBTRACT/FILTER) and value suggestions for search filters", + chain_filter_with_set_operations: BoolProperty( + name="Enable chained filters with set operations", + description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values", default=False, ) @@ -733,7 +733,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): default_parameters: DefaultParameters container_hide_show_isolate: bool mass_time_units_in_wizard: bool - search_filter_suggestions: bool + chain_filter_with_set_operations: bool def draw(self, context: bpy.types.Context) -> None: layout = self.layout @@ -916,7 +916,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "container_hide_show_isolate") layout.prop(self, "mass_time_units_in_wizard") - layout.prop(self, "search_filter_suggestions") + layout.prop(self, "chain_filter_with_set_operations") # Scene panel groups From 738a068474c93fcb27afeb380b20fb0fed90d0a8 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Thu, 18 Dec 2025 09:12:42 +0100 Subject: [PATCH 7/9] Refactor filter suggestion handling to streamline preference access and improve code clarity --- src/bonsai/bonsai/bim/helper.py | 148 ++++++++---------- .../bonsai/bim/module/drawing/operator.py | 5 +- .../bonsai/bim/module/search/operator.py | 16 +- src/bonsai/bonsai/bim/ui.py | 4 +- 4 files changed, 76 insertions(+), 97 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 16c6226326..481d4ea080 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -489,8 +489,6 @@ def draw_filter( data.load() sprops = tool.Search.get_search_props() - preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) if tool.Ifc.get(): row = layout.row(align=True) @@ -500,7 +498,7 @@ def draw_filter( if data.data["saved_searches"]: row.operator("bim.load_search", text="", icon="IMPORT").module = module row.operator("bim.save_search", text="", icon="EXPORT").module = module - if data.data["saved_searches"] and enable_suggestions: + if data.data["saved_searches"]: row.operator("bim.remove_search", text="", icon="REMOVE").module = module if module != "search": if module == "drawing_include": @@ -509,7 +507,7 @@ def draw_filter( row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE" row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE" row = layout.row(align=True) - if not enable_suggestions: + if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations: row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module else: if not filter_groups or not any(fg.filters for fg in filter_groups): @@ -537,7 +535,7 @@ def draw_filter( for j, ifc_filter in enumerate(filter_group.filters): if ifc_filter.type == "entity": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -549,15 +547,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "value", text="", icon="FILE_3D") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "attribute": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -569,15 +566,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "name", text="", icon="COPY_ID") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type - op.suggestion_type = "attribute_name" + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type + op.suggestion_type = "attribute_name" row.prop(ifc_filter, "value", text="") - if enable_suggestions and ifc_filter.name: + if ifc_filter.name: op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") op.group_index = i op.filter_index = j @@ -586,7 +582,7 @@ def draw_filter( op.suggestion_type = "attribute_value" elif ifc_filter.type == "type": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -598,15 +594,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "value", text="", icon="FILE_VOLUME") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "material": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -618,15 +613,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "value", text="", icon="MATERIAL") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "property": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -638,15 +632,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "pset", text="", icon="PROPERTIES") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type - op.suggestion_type = "pset" + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type + op.suggestion_type = "pset" row.prop(ifc_filter, "name", text="") - if enable_suggestions and ifc_filter.pset: + if ifc_filter.pset: op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") op.group_index = i op.filter_index = j @@ -655,7 +648,7 @@ def draw_filter( op.suggestion_type = "property_name" row.prop(ifc_filter, "comparison", text="") row.prop(ifc_filter, "value", text="") - if enable_suggestions and ifc_filter.pset and ifc_filter.name: + if ifc_filter.pset and ifc_filter.name: op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") op.group_index = i op.filter_index = j @@ -664,7 +657,7 @@ def draw_filter( op.suggestion_type = "property_value" elif ifc_filter.type == "classification": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -676,15 +669,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "value", text="", icon="OUTLINER") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "location": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -696,15 +688,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "value", text="", icon="PACKAGE") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "group": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -716,15 +707,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "value", text="", icon="OUTLINER_COLLECTION") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "parent": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -736,15 +726,14 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "value", text="", icon="FILE_PARENT") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type elif ifc_filter.type == "query": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -760,7 +749,7 @@ def draw_filter( row.prop(ifc_filter, "value", text="") elif ifc_filter.type == "instance": row = box.row(align=True) - if enable_suggestions and j > 0: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -772,12 +761,11 @@ def draw_filter( op.filter_index = j op.module = module row.prop(ifc_filter, "value", text="", icon="GRIP") - if enable_suggestions: - op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") - op.group_index = i - op.filter_index = j - op.module = module - op.filter_type = ifc_filter.type + op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM") + op.group_index = i + op.filter_index = j + op.module = module + op.filter_type = ifc_filter.type op = row.operator("bim.select_filter_elements", text="", icon="EYEDROPPER") op.group_index = i op.index = j diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 76eb3c7afa..57ebe25269 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3687,9 +3687,6 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): pset = tool.Pset.get_element_pset(element, "EPset_Drawing") assert pset - preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) - if self.filter_mode == "INCLUDE": filter_groups = props.include_filter_groups elif self.filter_mode == "EXCLUDE": @@ -3699,7 +3696,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): query = tool.Search.export_filter_query(filter_groups) or None - if enable_suggestions and query: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and query: filter_structure = [] for filter_group in filter_groups: group_data = [] diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 75dccd4d95..9402aebcff 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -662,10 +662,8 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): def _execute(self, context): module = getattr(self, "module", "search") - preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) - if not enable_suggestions: + if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations: if self.query == self.old_query: return @@ -676,20 +674,16 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): return def draw(self, context): - preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) - if not enable_suggestions: + if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations: row = self.layout.row() row.prop(self, "query", text="") def invoke(self, context, event): module = getattr(self, "module", "search") filter_groups = tool.Search.get_filter_groups(module) - preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) - if enable_suggestions: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations: filter_structure = [] for filter_group in filter_groups: group_data = [] @@ -766,10 +760,8 @@ class Search(Operator): else: assert_never(self.property_group) - preferences = tool.Blender.get_addon_preferences() - enable_suggestions = getattr(preferences, "chain_filter_with_set_operations", False) - if enable_suggestions: + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations: results = tool.Search.execute_filter_groups(props.filter_groups) else: results = ifcopenshell.util.selector.filter_elements( diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index eacd6ec472..0cac35da80 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -916,7 +916,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "container_hide_show_isolate") layout.prop(self, "mass_time_units_in_wizard") - layout.prop(self, "chain_filter_with_set_operations") + row = layout.row(align=True) + row.prop(self, "chain_filter_with_set_operations") + row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270" # Scene panel groups From affaa859ef1e32f49e70b36f4246610518434121 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Thu, 18 Dec 2025 10:20:28 +0100 Subject: [PATCH 8/9] Enhance filter operation modes with new preferences for chaining and default behaviors --- src/bonsai/bonsai/bim/helper.py | 14 ++++- .../bonsai/bim/module/search/operator.py | 4 +- src/bonsai/bonsai/bim/ui.py | 15 ++++- src/bonsai/bonsai/tool/search.py | 56 ++++++++++++++++++- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 481d4ea080..873d368fad 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -535,7 +535,12 @@ def draw_filter( for j, ifc_filter in enumerate(filter_group.filters): if ifc_filter.type == "entity": row = box.row(align=True) - if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: + preferences = tool.Blender.get_addon_preferences() + if preferences.chain_filter_with_set_operations: + show_mode_toggle = j > 0 + else: + show_mode_toggle = preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 # PR 7315 mode + if show_mode_toggle: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", @@ -749,7 +754,12 @@ def draw_filter( row.prop(ifc_filter, "value", text="") elif ifc_filter.type == "instance": row = box.row(align=True) - if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0: + preferences = tool.Blender.get_addon_preferences() + if preferences.chain_filter_with_set_operations: + show_mode_toggle = j > 0 + else: + show_mode_toggle = preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 # PR 7315 mode + if show_mode_toggle: mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} op = row.operator( "bim.toggle_filter_inclusion", diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 9402aebcff..f9e7885ebe 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -760,8 +760,8 @@ class Search(Operator): else: assert_never(self.property_group) - - if tool.Blender.get_addon_preferences().chain_filter_with_set_operations: + preferences = tool.Blender.get_addon_preferences() + if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: results = tool.Search.execute_filter_groups(props.filter_groups) else: results = ifcopenshell.util.selector.filter_elements( diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 0cac35da80..5c7ff01226 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -691,10 +691,15 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): ) chain_filter_with_set_operations: BoolProperty( - name="Enable chained filters with set operations", + name="NEW filter mode: Enable chained filters with set operations", description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values", default=False, ) + default_filter_with_set_operations_for_globalid_and_class: BoolProperty( + name="DEFAULT filter mode: Enable set operations for GlobalId/Class", + description="Enable ADD/SUBTRACT/FILTER toggle buttons on entity (Class) and instance (GlobalId) filters for the DEFAULT filter mode", + default=False, + ) if TYPE_CHECKING: svg2pdf_command: str @@ -734,6 +739,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): container_hide_show_isolate: bool mass_time_units_in_wizard: bool chain_filter_with_set_operations: bool + default_filter_with_set_operations_for_globalid_and_class: bool def draw(self, context: bpy.types.Context) -> None: layout = self.layout @@ -916,9 +922,14 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "container_hide_show_isolate") layout.prop(self, "mass_time_units_in_wizard") - row = layout.row(align=True) + layout.label(text="Filtering modes:") + box = layout.box() + row = box.row(align=True) row.prop(self, "chain_filter_with_set_operations") row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270" + row = box.row(align=True) + row.prop(self, "default_filter_with_set_operations_for_globalid_and_class") + row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/comment/27030" # Scene panel groups diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index ece6062f49..20c4b75387 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -184,23 +184,64 @@ class Search(bonsai.core.tool.Search): Within a single group chain, all filters chain sequentially with ADD/SUBTRACT/FILTER modes. Groups are combined with union (same as original " + " behavior). """ + preferences = tool.Blender.get_addon_preferences() + #print(f"\n{'='*80}") + #print(f"DEBUG: execute_filter_groups - Starting execution") + #print(f"DEBUG: Preferences - chain_filter_with_set_operations: {preferences.chain_filter_with_set_operations}") + #print(f"DEBUG: Preferences - default_filter_with_set_operations_for_globalid_and_class: {preferences.default_filter_with_set_operations_for_globalid_and_class}") + #print(f"DEBUG: Total filter groups: {len(filter_groups)}") + all_group_results = [] - for filter_group in filter_groups: + for group_idx, filter_group in enumerate(filter_groups): + #print(f"\n--- Group {group_idx} ---") group_results = set() for filter_index, ifc_filter in enumerate(filter_group.filters): + #print(f"\n Filter {filter_index}:") + #print(f" Type: {ifc_filter.type}") + #print(f" Value: {ifc_filter.value}") + #print(f" filter_mode property: {ifc_filter.filter_mode}") + if not ifc_filter.value: + #print(f" SKIPPED: No value") continue query = cls._export_single_filter(ifc_filter) if not query: + #print(f" SKIPPED: No query generated") continue - mode = "ADD" if filter_index == 0 else ifc_filter.filter_mode + #print(f" Generated query: {query}") + #print(f" Current group_results size before this filter: {len(group_results)}") + + # Determine the mode for this filter + if filter_index == 0: + mode = "ADD" # First filter is always ADD + #print(f" Mode decision: ADD (first filter)") + else: + # For entity and instance filters, check if set operations are enabled + if ifc_filter.type in ["entity", "instance"]: + # Use filter_mode only if chain mode OR default mode preference is enabled + if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: + mode = ifc_filter.filter_mode + #print(f" Mode decision: {mode} (entity/instance with preference enabled)") + else: + # Default behavior: sequential filtering (FILTER mode) + mode = "FILTER" if group_results else "ADD" + #print(f" Mode decision: {mode} (entity/instance default behavior)") + else: + # For other filter types, use chain mode if enabled, otherwise FILTER + if preferences.chain_filter_with_set_operations: + mode = ifc_filter.filter_mode + #print(f" Mode decision: {mode} (other type with chain mode)") + else: + mode = "FILTER" if group_results else "ADD" + #print(f" Mode decision: {mode} (other type default behavior)") if mode == "ADD": results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + #print(f" ADD: Found {len(results)} elements, adding to group_results") group_results.update(results) elif mode == "SUBTRACT": @@ -209,9 +250,11 @@ class Search(bonsai.core.tool.Search): elements_to_remove = ifcopenshell.util.selector.filter_elements( tool.Ifc.get(), query_without_prefix, elements=group_results ) + #print(f" SUBTRACT: Removing {len(elements_to_remove)} elements from group_results") group_results -= elements_to_remove else: results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + #print(f" SUBTRACT: group_results empty, adding {len(results)} elements (fallback to ADD)") group_results.update(results) elif mode == "FILTER": @@ -219,11 +262,16 @@ class Search(bonsai.core.tool.Search): results = ifcopenshell.util.selector.filter_elements( tool.Ifc.get(), query, elements=group_results ) + #print(f" FILTER: Filtering group_results, result: {len(results)} elements") group_results = results else: results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) + #print(f" FILTER: group_results empty, found {len(results)} elements (fallback to ADD)") group_results.update(results) + + #print(f" Group results size after this filter: {len(group_results)}") + #print(f"\n Group {group_idx} final size: {len(group_results)}") if group_results: all_group_results.append(group_results) @@ -231,6 +279,10 @@ class Search(bonsai.core.tool.Search): for group_results in all_group_results: final_results.update(group_results) + #print(f"\n{'='*80}") + #print(f"DEBUG: Final combined results: {len(final_results)} elements") + #print(f"{'='*80}\n") + return final_results @classmethod From 59c9b162761b2f3c62d3a08f79e1bccf13591fd8 Mon Sep 17 00:00:00 2001 From: falken10vdl Date: Thu, 18 Dec 2025 10:56:18 +0100 Subject: [PATCH 9/9] Migrate old filter prefixes to new filter_mode system and enhance filter handling based on user preferences --- .../bonsai/bim/module/search/operator.py | 10 ++++ src/bonsai/bonsai/tool/search.py | 59 +++++-------------- 2 files changed, 24 insertions(+), 45 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index f9e7885ebe..58c773af17 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -761,7 +761,17 @@ class Search(Operator): assert_never(self.property_group) preferences = tool.Blender.get_addon_preferences() + + # Migrate old ! prefix filters to new filter_mode system when preferences are enabled if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: + for filter_group in props.filter_groups: + for ifc_filter in filter_group.filters: + if ifc_filter.type not in ["entity", "instance"]: + continue + if ifc_filter.value.startswith("!"): + ifc_filter.value = ifc_filter.value[1:] + ifc_filter.filter_mode = "SUBTRACT" + results = tool.Search.execute_filter_groups(props.filter_groups) else: results = ifcopenshell.util.selector.filter_elements( diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index 20c4b75387..36536f5a1a 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -123,21 +123,28 @@ class Search(bonsai.core.tool.Search): @classmethod def _export_single_filter(cls, ifc_filter: BIMFacet) -> str: + preferences = tool.Blender.get_addon_preferences() + if ifc_filter.type == "instance": if "bpy.data.texts" in ifc_filter.value: data_name = ifc_filter.value.split("bpy.data.texts")[1][2:-2] value = bpy.data.texts[data_name].as_string() else: value = ifc_filter.value - value = value.lstrip("!") - if ifc_filter.filter_mode == "SUBTRACT": - value = f"!{value}" + + if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: + value = value.lstrip("!") + if ifc_filter.filter_mode == "SUBTRACT": + value = f"!{value}" return value + elif ifc_filter.type == "entity": value = ifc_filter.value - value = value.lstrip("!") - if ifc_filter.filter_mode == "SUBTRACT": - value = f"!{value}" + + if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: + value = value.lstrip("!") + if ifc_filter.filter_mode == "SUBTRACT": + value = f"!{value}" return value elif ifc_filter.type == "attribute": if not ifc_filter.name: @@ -185,63 +192,36 @@ class Search(bonsai.core.tool.Search): Groups are combined with union (same as original " + " behavior). """ preferences = tool.Blender.get_addon_preferences() - #print(f"\n{'='*80}") - #print(f"DEBUG: execute_filter_groups - Starting execution") - #print(f"DEBUG: Preferences - chain_filter_with_set_operations: {preferences.chain_filter_with_set_operations}") - #print(f"DEBUG: Preferences - default_filter_with_set_operations_for_globalid_and_class: {preferences.default_filter_with_set_operations_for_globalid_and_class}") - #print(f"DEBUG: Total filter groups: {len(filter_groups)}") all_group_results = [] for group_idx, filter_group in enumerate(filter_groups): - #print(f"\n--- Group {group_idx} ---") group_results = set() for filter_index, ifc_filter in enumerate(filter_group.filters): - #print(f"\n Filter {filter_index}:") - #print(f" Type: {ifc_filter.type}") - #print(f" Value: {ifc_filter.value}") - #print(f" filter_mode property: {ifc_filter.filter_mode}") - if not ifc_filter.value: - #print(f" SKIPPED: No value") continue query = cls._export_single_filter(ifc_filter) if not query: - #print(f" SKIPPED: No query generated") continue - #print(f" Generated query: {query}") - #print(f" Current group_results size before this filter: {len(group_results)}") - - # Determine the mode for this filter if filter_index == 0: - mode = "ADD" # First filter is always ADD - #print(f" Mode decision: ADD (first filter)") + mode = "ADD" else: - # For entity and instance filters, check if set operations are enabled if ifc_filter.type in ["entity", "instance"]: - # Use filter_mode only if chain mode OR default mode preference is enabled if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: mode = ifc_filter.filter_mode - #print(f" Mode decision: {mode} (entity/instance with preference enabled)") else: - # Default behavior: sequential filtering (FILTER mode) mode = "FILTER" if group_results else "ADD" - #print(f" Mode decision: {mode} (entity/instance default behavior)") else: - # For other filter types, use chain mode if enabled, otherwise FILTER if preferences.chain_filter_with_set_operations: mode = ifc_filter.filter_mode - #print(f" Mode decision: {mode} (other type with chain mode)") else: mode = "FILTER" if group_results else "ADD" - #print(f" Mode decision: {mode} (other type default behavior)") if mode == "ADD": results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) - #print(f" ADD: Found {len(results)} elements, adding to group_results") group_results.update(results) elif mode == "SUBTRACT": @@ -250,11 +230,9 @@ class Search(bonsai.core.tool.Search): elements_to_remove = ifcopenshell.util.selector.filter_elements( tool.Ifc.get(), query_without_prefix, elements=group_results ) - #print(f" SUBTRACT: Removing {len(elements_to_remove)} elements from group_results") group_results -= elements_to_remove else: results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) - #print(f" SUBTRACT: group_results empty, adding {len(results)} elements (fallback to ADD)") group_results.update(results) elif mode == "FILTER": @@ -262,16 +240,11 @@ class Search(bonsai.core.tool.Search): results = ifcopenshell.util.selector.filter_elements( tool.Ifc.get(), query, elements=group_results ) - #print(f" FILTER: Filtering group_results, result: {len(results)} elements") group_results = results else: results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) - #print(f" FILTER: group_results empty, found {len(results)} elements (fallback to ADD)") group_results.update(results) - - #print(f" Group results size after this filter: {len(group_results)}") - #print(f"\n Group {group_idx} final size: {len(group_results)}") if group_results: all_group_results.append(group_results) @@ -279,10 +252,6 @@ class Search(bonsai.core.tool.Search): for group_results in all_group_results: final_results.update(group_results) - #print(f"\n{'='*80}") - #print(f"DEBUG: Final combined results: {len(final_results)} elements") - #print(f"{'='*80}\n") - return final_results @classmethod