diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 6d495849fe..e874983074 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -302,10 +302,9 @@ class CreateDrawing(bpy.types.Operator): # Force Blender to process all pending operations for area in context.screen.areas: area.tag_redraw() - + # Process events to let Blender finish internal cleanup - bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) - + bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1) self.camera = context.scene.camera assert (camera_element := tool.Ifc.get_entity(self.camera)) @@ -3713,16 +3712,16 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): assert element pset = tool.Pset.get_element_pset(element, "EPset_Drawing") assert pset - + if self.filter_mode == "INCLUDE": filter_groups = props.include_filter_groups elif self.filter_mode == "EXCLUDE": filter_groups = props.exclude_filter_groups else: return - + query = tool.Search.export_filter_query(filter_groups) or None - + if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and query: filter_structure = [] for filter_group in filter_groups: @@ -3738,20 +3737,16 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): } group_data.append(filter_data) filter_structure.append(group_data) - - value = json.dumps({ - "type": "BBIM_Search", - "query": query, - "filter_structure": filter_structure - }) + + 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/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index e854f9ed32..1a25b387dd 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -484,7 +484,7 @@ class BIM_PT_sheets(Panel): for annotation in ifc_file.by_type("IfcAnnotation"): if annotation.ObjectType != "DRAWING": continue - + Annotation_Name = annotation.Name.replace(",", "") # Remove commas if Annotation_Name == drawingname: drawingid = annotation.id() diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 58c773af17..2c7a506e78 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -54,18 +54,18 @@ def draw_text_editor_header(self, context): 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] @@ -73,23 +73,23 @@ def update_filter_search_value(self: "FilterValueSuggestions", context: bpy.type 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): + 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 @@ -104,7 +104,7 @@ def update_filter_search_value(self: "FilterValueSuggestions", context: bpy.type ifc_filter.value = value else: ifc_filter.value = value - + if self.first_launch: self.first_launch = False else: @@ -122,7 +122,7 @@ class FilterValueSuggestions(Operator): module: StringProperty(default="search") filter_type: StringProperty() suggestion_type: StringProperty(default="value") - + first_launch: BoolProperty(default=True, options={"SKIP_SAVE"}) search_value: StringProperty( name="Search", @@ -158,10 +158,10 @@ class FilterValueSuggestions(Operator): 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", @@ -172,7 +172,7 @@ class FilterValueSuggestions(Operator): "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}" @@ -187,7 +187,7 @@ class FilterValueSuggestions(Operator): label = "Select Property Set" else: label = label_map.get(ifc_filter.type, "Select Value") - + row = layout.row() row.label(text=label) row = layout.row() @@ -202,7 +202,7 @@ class FilterValueSuggestions(Operator): 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": @@ -231,58 +231,58 @@ class FilterValueSuggestions(Operator): 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: + if hasattr(current, "Name") and current.Name: path.insert(0, current.Name) - + parent = None - - if hasattr(current, 'Decomposes') and current.Decomposes: + + if hasattr(current, "Decomposes") and current.Decomposes: for rel in current.Decomposes: - if hasattr(rel, 'RelatingObject'): + if hasattr(rel, "RelatingObject"): parent = rel.RelatingObject break - - if not parent and hasattr(current, 'ContainedInStructure') and current.ContainedInStructure: + + if not parent and hasattr(current, "ContainedInStructure") and current.ContainedInStructure: for rel in current.ContainedInStructure: - if hasattr(rel, 'RelatingStructure'): + 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]) + superclass_chain = " > ".join(chain_names[: i + 1]) all_classes.add(f"{superclass_chain} (superclass)") else: all_classes.add(class_name) @@ -290,7 +290,7 @@ class FilterValueSuggestions(Operator): all_classes.add(class_name) except: continue - + return all_classes def get_type_suggestions(self, ifc_file): @@ -346,60 +346,60 @@ class FilterValueSuggestions(Operator): try: element = ifc_file.by_id(element_id) has_children = False - - if hasattr(element, 'IsDecomposedBy') and element.IsDecomposedBy: + + if hasattr(element, "IsDecomposedBy") and element.IsDecomposedBy: has_children = True - elif hasattr(element, 'ContainsElements') and element.ContainsElements: + elif hasattr(element, "ContainsElements") and element.ContainsElements: has_children = True - elif hasattr(element, 'HasOpenings') and element.HasOpenings: + 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 + 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: + 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): @@ -407,8 +407,8 @@ class FilterValueSuggestions(Operator): 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'): + 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) @@ -421,8 +421,8 @@ class FilterValueSuggestions(Operator): 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'): + 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: @@ -438,8 +438,8 @@ class FilterValueSuggestions(Operator): 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'): + 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: @@ -449,9 +449,13 @@ class FilterValueSuggestions(Operator): try: value = prop.NominalValue.wrappedValue if value is not None and value != "": - if not hasattr(value, 'is_a') and not isinstance(value, (tuple, list)): + 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("("): + if not str_value.startswith("#") and not str_value.startswith( + "(" + ): property_values.add(str_value) except: continue @@ -462,7 +466,7 @@ class FilterValueSuggestions(Operator): 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: @@ -470,7 +474,7 @@ class FilterValueSuggestions(Operator): ifc_classes.add(element.is_a()) except: continue - + for ifc_class in ifc_classes: try: entity = schema.declaration_by_name(ifc_class).as_entity() @@ -479,30 +483,30 @@ class FilterValueSuggestions(Operator): 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)): + 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 @@ -580,14 +584,14 @@ class ToggleFilterInclusion(Operator): 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"} @@ -615,40 +619,40 @@ class ApplyFilterFromText(Operator, tool.Ifc.Operator): 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': + 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'} - + 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'} + return {"CANCELLED"} + + return {"FINISHED"} class EditFilterQuery(Operator, tool.Ifc.Operator): @@ -662,7 +666,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): def _execute(self, context): module = getattr(self, "module", "search") - + if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations: if self.query == self.old_query: return @@ -674,7 +678,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): return def draw(self, context): - + if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations: row = self.layout.row() row.prop(self, "query", text="") @@ -698,39 +702,37 @@ class EditFilterQuery(Operator, tool.Ifc.Operator): } 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 - } - + 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' - + new_area.type = "TEXT_EDITOR" + text_space = None for space in new_area.spaces: - if space.type == 'TEXT_EDITOR': + 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'} + + self.report( + {"INFO"}, "Compact text editor opened. Edit JSON and click 'Apply Filter Configuration' in header" + ) + return {"FINISHED"} else: self.query = tool.Search.export_filter_query(filter_groups) @@ -761,9 +763,12 @@ 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: + 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"]: @@ -771,7 +776,7 @@ class Search(Operator): 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( @@ -843,7 +848,7 @@ class SaveSearch(Operator, tool.Ifc.Operator): try: query = tool.Search.export_filter_query(filter_groups) results = tool.Search.execute_filter_groups(filter_groups) - + filter_structure = [] for filter_group in filter_groups: group_data = [] @@ -863,11 +868,7 @@ class SaveSearch(Operator, tool.Ifc.Operator): self.report({"ERROR"}, "Error occurred trying save search.") return - description = json.dumps({ - "type": "BBIM_Search", - "query": query, - "filter_structure": filter_structure - }) + description = json.dumps({"type": "BBIM_Search", "query": query, "filter_structure": filter_structure}) ifc_file = tool.Ifc.get() group = next( ( @@ -908,7 +909,7 @@ 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)) - + 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) @@ -939,7 +940,7 @@ class RemoveSearch(Operator, tool.Ifc.Operator): 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) @@ -955,6 +956,7 @@ class RemoveSearch(Operator, tool.Ifc.Operator): 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) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index f0455a4ba1..b687134afc 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -2125,7 +2125,7 @@ class Drawing(bonsai.core.tool.Drawing): command = command.replace(variable, value) # Defensive: skip if command[2:-2] is None or 'None' command_content = command[2:-2] - if command_content is None or str(command_content).strip().lower() == 'none': + if command_content is None or str(command_content).strip().lower() == "none": text = text.replace(original_command, "") else: try: diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index 36536f5a1a..a0e2e17456 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -51,17 +51,17 @@ class Search(bonsai.core.tool.Search): 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", "") @@ -112,11 +112,11 @@ class Search(bonsai.core.tool.Search): for ifc_filter in filter_group.filters: if not ifc_filter.value: continue - + 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) @@ -124,24 +124,30 @@ 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 - - if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: + + 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 - - if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: + + 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}" @@ -192,25 +198,28 @@ class Search(bonsai.core.tool.Search): Groups are combined with union (same as original " + " behavior). """ preferences = tool.Blender.get_addon_preferences() - + all_group_results = [] - + for group_idx, filter_group in enumerate(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 - + if filter_index == 0: mode = "ADD" else: if ifc_filter.type in ["entity", "instance"]: - if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class: + if ( + preferences.chain_filter_with_set_operations + or preferences.default_filter_with_set_operations_for_globalid_and_class + ): mode = ifc_filter.filter_mode else: mode = "FILTER" if group_results else "ADD" @@ -219,11 +228,11 @@ class Search(bonsai.core.tool.Search): mode = ifc_filter.filter_mode else: mode = "FILTER" if group_results else "ADD" - + 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 @@ -234,7 +243,7 @@ class Search(bonsai.core.tool.Search): 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( @@ -244,14 +253,14 @@ class Search(bonsai.core.tool.Search): 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 @@ -259,24 +268,24 @@ class Search(bonsai.core.tool.Search): 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 @@ -318,10 +327,10 @@ class Search(bonsai.core.tool.Search): 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 @@ -331,14 +340,14 @@ class Search(bonsai.core.tool.Search): 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