diff --git a/docs/dev-notes/Additional_Selection_and_Deselection_Tools.md b/docs/dev-notes/Additional_Selection_and_Deselection_Tools.md index 89e675cdd2..36d53243e7 100644 --- a/docs/dev-notes/Additional_Selection_and_Deselection_Tools.md +++ b/docs/dev-notes/Additional_Selection_and_Deselection_Tools.md @@ -27,6 +27,7 @@ One modifier scheme, applied uniformly across nine operators: | CTRL+Click | **filter** the selection to matches only (selects nothing new) | | CTRL+SHIFT+Click | legacy plain-CTRL function, where one existed | | ALT+Click | also unhide matches (viewport + local hide) — from the base branch | +| CTRL+ALT+Click | `select_similar`, `select_similar_type`, `select_by_material`, `select_group_elements`, `select_aggregate`: regex-search dialog (see below) | Operators covered: `bim.select_similar`, `bim.select_ifc_class`, `bim.select_similar_type`, `bim.select_by_material`, `bim.select_similar_container`, @@ -63,6 +64,40 @@ Operators covered: `bim.select_similar`, `bim.select_ifc_class`, - **`select_ifc_class` filter matches subtypes** (`element.is_a(cls)`), consistent with normal select mode which uses `file.by_type(cls)` (also subtype-inclusive). +### Regex-search dialog on `select_similar`, `select_similar_type`, `select_by_material`, `select_group_elements`, `select_aggregate` (CTRL+ALT+Click) + +On `select_aggregate` the pattern is prefilled with the active object's **aggregate +name** and matched against every `IfcRelAggregates.RelatingObject` that `is_a +IfcElement` (spatial decomposition — project/site/storey — deliberately excluded); +the dialog additionally exposes "Also Select Parts" (+ "One Level Deep") since the +panel's two button variants collapse into one dialog. Union of matched aggregates +(+ parts via `get_parts`/`get_decomposition`) through one `Spatial.select_products` +call; clipboard query `parent = /.*foo.*/`. + +On `select_similar_type` the pattern is prefilled with (and matched against) the active +object's **type name**; the clipboard query is `type = /.*foo.*/`. On +`select_by_material` it is the active object's **resolved material name** (via the +#7940 helpers: usage → set, clicked-layer index as hint, `_get_name`), falling back to +the clicked material row's name; clipboard query `material = /.*foo.*/`. On +`select_group_elements` it is the clicked group row's **name**, matched against all +`IfcGroup` names in the file (unnamed groups never match); the union of the matching +groups' elements (recursive by default) goes through a single +`Spatial.select_products` call — union first, so FILTER cannot wrongly intersect +per-group; clipboard query `group = /.*foo.*/`. Otherwise identical to the +`select_similar` behavior below. + +Opens a props dialog prefilled with the active object's value for the clicked key; the +(possibly edited) text is compiled as an unanchored Python regex (`re.search`, so +entering `foo` behaves like `.*foo.*`) and applied via an Add / Remove / Filter +dropdown, plus an "Also Unhide Hidden Objects" checkbox (reuses `should_unhide`; in +Add/Remove it sweeps `scene.objects` instead of `visible_objects` and clears both +hide flags on matches; a no-op in Filter since selected objects are visible). CTRL+ALT was free in practice: ALT (unhide) is a no-op in filter mode, which +plain CTRL triggers. Invalid patterns error out and cancel. The equivalent selector +query (`Key = /.*foo.*/`) is copied to the clipboard. Note the prefill is the raw +value — values containing regex metacharacters (e.g. `(`) need escaping before OK. +Overriding `draw()` for the dialog means the F9 redo panel no longer auto-lists the +operator's internal properties for normal runs (it was exposing internals anyway). + ### Deliberately overwritten SHIFT bindings (to be reworked later) Two operators already used SHIFT; the owner chose to overwrite them and revisit with diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index 64ad5005ba..1527892899 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import re from typing import TYPE_CHECKING import bpy @@ -269,15 +270,36 @@ class BIM_OT_select_aggregate(bpy.types.Operator): should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) remove_from_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) filter_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + use_regex: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + regex_pattern: bpy.props.StringProperty( + name="Pattern", + description='Python regular expression matched anywhere in each aggregate\'s name, e.g. "foo" matches ".*foo.*"', + ) + regex_mode: bpy.props.EnumProperty( + name="Action", + items=[ + ("ADD", "Add to Selection", "Select aggregates whose name matches the pattern"), + ("REMOVE", "Remove from Selection", "Deselect aggregates whose name matches the pattern"), + ("FILTER", "Filter Selection", "Keep only already selected aggregates whose name matches the pattern"), + ], + default="ADD", + ) @classmethod def description(cls, context, properties): if properties.select_parts: - return "Select Aggregate and Parts.\n\nSHIFT+Click to remove from selection set\nCTRL+Click to filter selection to matching objects only\nCTRL+SHIFT+Click to select only one level deep\nALT+Click to also unhide hidden objects (viewport and local hide)" + return "Select Aggregate and Parts.\n\nSHIFT+Click to remove from selection set\nCTRL+Click to filter selection to matching objects only\nCTRL+SHIFT+Click to select only one level deep\nCTRL+ALT+Click to search aggregate names by regex in a dialog\nALT+Click to also unhide hidden objects (viewport and local hide)" else: - return "Select Aggregate\n\nSHIFT+Click to remove from selection set\nCTRL+Click to filter selection to matching objects only\nALT+Click to also unhide hidden objects (viewport and local hide)" + return "Select Aggregate\n\nSHIFT+Click to remove from selection set\nCTRL+Click to filter selection to matching objects only\nCTRL+ALT+Click to search aggregate names by regex in a dialog\nALT+Click to also unhide hidden objects (viewport and local hide)" def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.ctrl and event.alt and not event.shift: + self.use_regex = True + if context.active_object and (element := tool.Ifc.get_entity(context.active_object)): + aggregate = ifcopenshell.util.element.get_aggregate(element) + if aggregate and aggregate.Name: + self.regex_pattern = aggregate.Name + return context.window_manager.invoke_props_dialog(self) if event.type == "LEFTMOUSE" and event.ctrl and event.shift: self.one_level_deep = True self.should_unhide = event.alt @@ -285,7 +307,66 @@ class BIM_OT_select_aggregate(bpy.types.Operator): self.filter_selection = event.ctrl and not event.shift return self.execute(context) + def draw(self, context): + layout = self.layout + if not self.use_regex: + return + layout.prop(self, "regex_pattern") + layout.prop(self, "regex_mode") + layout.prop(self, "select_parts", text="Also Select Parts") + if self.select_parts: + layout.prop(self, "one_level_deep") + layout.prop(self, "should_unhide", text="Also Unhide Hidden Objects") + + def _execute_regex(self, context): + try: + pattern = re.compile(self.regex_pattern) + except re.error as e: + self.report({"ERROR"}, f"Invalid regular expression: {e}") + return {"CANCELLED"} + + aggregates = {} + for rel in tool.Ifc.get().by_type("IfcRelAggregates"): + aggregate = rel.RelatingObject + if not aggregate.is_a("IfcElement"): + continue + if not aggregate.Name or not pattern.search(aggregate.Name): + continue + aggregates[aggregate.id()] = aggregate + + products = set() + for aggregate in aggregates.values(): + products.add(aggregate) + if self.select_parts: + if self.one_level_deep: + products.update(ifcopenshell.util.element.get_parts(aggregate)) + else: + products.update(ifcopenshell.util.element.get_decomposition(aggregate)) + + tool.Spatial.select_products( + products, + unhide=self.should_unhide, + remove=self.regex_mode == "REMOVE", + filter_selection=self.regex_mode == "FILTER", + ) + + if self.regex_mode == "FILTER": + verb = "Filtered selection to" + elif self.regex_mode == "REMOVE": + verb = "Deselected" + else: + verb = "Selected" + result = f"parent = /.*{self.regex_pattern}.*/" + bpy.context.window_manager.clipboard = result + self.report( + {"INFO"}, + f"{verb} {len(aggregates)} aggregates matching ({result}); query copied to the clipboard.", + ) + return {"FINISHED"} + def execute(self, context): + if self.use_regex: + return self._execute_regex(context) keep_current_selection = self.remove_from_selection or self.filter_selection if keep_current_selection: objects = [context.active_object] if context.active_object else [] diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index ed7d0d0499..0870c31d91 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import re from typing import TYPE_CHECKING, get_args import bpy @@ -200,6 +201,7 @@ class SelectGroupElements(bpy.types.Operator): "\nSHIFT + CLICK to remove from selection set" "\nCTRL + CLICK to filter selection to matching objects only" "\nCTRL + SHIFT + CLICK to exclude children" + "\nCTRL + ALT + CLICK to search group names by regex in a dialog" "\nALT + CLICK to also unhide hidden objects (viewport and local hide)" ) group: bpy.props.IntProperty() @@ -207,15 +209,80 @@ class SelectGroupElements(bpy.types.Operator): should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) remove_from_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) filter_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + use_regex: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + regex_pattern: bpy.props.StringProperty( + name="Pattern", + description='Python regular expression matched anywhere in each group\'s name, e.g. "foo" matches ".*foo.*"', + ) + regex_mode: bpy.props.EnumProperty( + name="Action", + items=[ + ("ADD", "Add to Selection", "Select elements of groups whose name matches the pattern"), + ("REMOVE", "Remove from Selection", "Deselect elements of groups whose name matches the pattern"), + ("FILTER", "Filter Selection", "Keep only already selected elements of groups whose name matches the pattern"), + ], + default="ADD", + ) def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.ctrl and event.alt and not event.shift: + self.use_regex = True + if self.group and (name := tool.Ifc.get().by_id(self.group).Name): + self.regex_pattern = name + return context.window_manager.invoke_props_dialog(self) self.is_recursive = not (event.ctrl and event.shift) self.should_unhide = event.alt self.remove_from_selection = event.shift and not event.ctrl self.filter_selection = event.ctrl and not event.shift return self.execute(context) + def draw(self, context): + layout = self.layout + if not self.use_regex: + return + layout.prop(self, "regex_pattern") + layout.prop(self, "regex_mode") + layout.prop(self, "should_unhide", text="Also Unhide Hidden Objects") + + def _execute_regex(self, context): + try: + pattern = re.compile(self.regex_pattern) + except re.error as e: + self.report({"ERROR"}, f"Invalid regular expression: {e}") + return {"CANCELLED"} + + products = set() + matched_groups = 0 + for group in tool.Ifc.get().by_type("IfcGroup"): + if not group.Name or not pattern.search(group.Name): + continue + matched_groups += 1 + products.update(ifcopenshell.util.element.get_grouped_by(group, is_recursive=self.is_recursive)) + + tool.Spatial.select_products( + products, + unhide=self.should_unhide, + remove=self.regex_mode == "REMOVE", + filter_selection=self.regex_mode == "FILTER", + ) + + if self.regex_mode == "FILTER": + verb = "Filtered selection to" + elif self.regex_mode == "REMOVE": + verb = "Deselected" + else: + verb = "Selected" + result = f"group = /.*{self.regex_pattern}.*/" + bpy.context.window_manager.clipboard = result + self.report( + {"INFO"}, + f"{verb} elements of {matched_groups} groups matching ({result}); query copied to the clipboard.", + ) + return {"FINISHED"} + def execute(self, context): + if self.use_regex: + return self._execute_regex(context) tool.Spatial.select_products( ifcopenshell.util.element.get_grouped_by(tool.Ifc.get().by_id(self.group), is_recursive=self.is_recursive), unhide=self.should_unhide, diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 5cd03f6b3d..f412fc0fa3 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import json +import re from typing import TYPE_CHECKING, Any, Literal, Union import bpy @@ -61,20 +62,111 @@ class DisableEditingMaterials(bpy.types.Operator): class SelectByMaterial(bpy.types.Operator): bl_idname = "bim.select_by_material" bl_label = "Select By Material" - bl_description = "Select objects using the provided material\n\nSHIFT+Click to remove from selection set\nCTRL+Click to filter selection to matching objects only\nALT+Click to also unhide hidden objects (viewport and local hide)" + bl_description = "Select objects using the provided material\n\nSHIFT+Click to remove from selection set\nCTRL+Click to filter selection to matching objects only\nCTRL+ALT+Click to search material names by regex in a dialog\nALT+Click to also unhide hidden objects (viewport and local hide)" bl_options = {"REGISTER", "UNDO"} material: bpy.props.IntProperty() should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) remove_from_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) filter_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + use_regex: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + regex_pattern: bpy.props.StringProperty( + name="Pattern", + description='Python regular expression matched anywhere in each object\'s material name, e.g. "foo" matches ".*foo.*"', + ) + regex_mode: bpy.props.EnumProperty( + name="Action", + items=[ + ("ADD", "Add to Selection", "Select objects whose material name matches the pattern"), + ("REMOVE", "Remove from Selection", "Deselect objects whose material name matches the pattern"), + ("FILTER", "Filter Selection", "Keep only already selected objects whose material name matches the pattern"), + ], + default="ADD", + ) def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.ctrl and event.alt and not event.shift: + self.use_regex = True + layer_index = None + if self.material: + layer_index = self._get_layer_index(tool.Ifc.get().by_id(self.material)) + name = None + if context.active_object: + name = self._get_material_name(context.active_object, layer_index) + if name is None and self.material: + name = self._get_name(tool.Ifc.get().by_id(self.material)) + if name is not None: + self.regex_pattern = name + return context.window_manager.invoke_props_dialog(self) self.should_unhide = event.alt self.remove_from_selection = event.shift and not event.ctrl self.filter_selection = event.ctrl and not event.shift return self.execute(context) + def draw(self, context): + layout = self.layout + if not self.use_regex: + return + layout.prop(self, "regex_pattern") + layout.prop(self, "regex_mode") + layout.prop(self, "should_unhide", text="Also Unhide Hidden Objects") + + def _get_material_name(self, obj, layer_index): + element = tool.Ifc.get_entity(obj) + if not element: + return None + mat = ifcopenshell.util.element.get_material(element) + if not mat: + return None + resolved = self._resolve_material(mat, layer_index) + if not resolved: + return None + return self._get_name(resolved) + + def _execute_regex(self, context): + try: + pattern = re.compile(self.regex_pattern) + except re.error as e: + self.report({"ERROR"}, f"Invalid regular expression: {e}") + return {"CANCELLED"} + + layer_index = None + if self.material: + layer_index = self._get_layer_index(tool.Ifc.get().by_id(self.material)) + + count = 0 + if self.regex_mode == "FILTER": + for obj in context.selected_objects: + name = self._get_material_name(obj, layer_index) + if name is not None and pattern.search(name): + count += 1 + else: + obj.select_set(False) + else: + remove = self.regex_mode == "REMOVE" + objects = context.scene.objects if self.should_unhide else context.visible_objects + for obj in objects: + name = self._get_material_name(obj, layer_index) + if name is not None and pattern.search(name): + if self.should_unhide: + obj.hide_viewport = False + obj.hide_set(False) + obj.select_set(not remove) + count += 1 + + if self.regex_mode == "FILTER": + verb = "Filtered selection to" + elif self.regex_mode == "REMOVE": + verb = "Deselected" + else: + verb = "Selected" + result = f"material = /.*{self.regex_pattern}.*/" + bpy.context.window_manager.clipboard = result + self.report({"INFO"}, f"{verb} {count} objects matching ({result}); query copied to the clipboard.") + return {"FINISHED"} + def execute(self, context): + if self.use_regex: + return self._execute_regex(context) # Determine the layer index hint from the explicit material prop, if any. # When the user clicks a specific layer in the UI, self.material is that # layer's IfcMaterial. We find its index so we can pull the same layer diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 02612db198..d9930a2d4d 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -18,6 +18,7 @@ import bisect import json +import re import traceback from typing import TYPE_CHECKING, Any, Literal, assert_never, get_args @@ -1469,10 +1470,24 @@ class SelectSimilar(Operator): remove_from_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) filter_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + use_regex: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + regex_pattern: bpy.props.StringProperty( + name="Pattern", + description='Python regular expression matched anywhere in each object\'s value, e.g. "foo" matches ".*foo.*"', + ) + regex_mode: bpy.props.EnumProperty( + name="Action", + items=[ + ("ADD", "Add to Selection", "Select objects whose value matches the pattern"), + ("REMOVE", "Remove from Selection", "Deselect objects whose value matches the pattern"), + ("FILTER", "Filter Selection", "Keep only already selected objects whose value matches the pattern"), + ], + default="ADD", + ) @classmethod def description(cls, context, properties): - base = "Select objects with a similar value\n\nSHIFT+CLICK remove from selection set.\nCTRL+CLICK filter selection to matching objects only.\nALT+CLICK also unhide hidden objects (viewport and local hide)." + base = "Select objects with a similar value\n\nSHIFT+CLICK remove from selection set.\nCTRL+CLICK filter selection to matching objects only.\nCTRL+ALT+CLICK search by regex in a dialog.\nALT+CLICK also unhide hidden objects (viewport and local hide)." key = getattr(properties, "key", None) active = context.active_object @@ -1497,12 +1512,27 @@ class SelectSimilar(Operator): return False def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.ctrl and event.alt and not event.shift: + self.use_regex = True + key = "predefined_type" if self.key == "PredefinedType" else self.key + value = self._get_value(context.active_object, key) if context.active_object else None + if value is not None: + self.regex_pattern = str(value) + return context.window_manager.invoke_props_dialog(self) self.calculate_sum = event.ctrl and event.shift and event.type == "LEFTMOUSE" self.remove_from_selection = event.shift and not event.ctrl and event.type == "LEFTMOUSE" self.filter_selection = event.ctrl and not event.shift and event.type == "LEFTMOUSE" self.should_unhide = event.alt return self.execute(context) + def draw(self, context): + layout = self.layout + if not self.use_regex: + return + layout.prop(self, "regex_pattern") + layout.prop(self, "regex_mode") + layout.prop(self, "should_unhide", text="Also Unhide Hidden Objects") + def execute(self, context): self.calculated_sum = 0 # reset if run before key = "predefined_type" if self.key == "PredefinedType" else self.key @@ -1510,6 +1540,9 @@ class SelectSimilar(Operator): tolerance = prefs.doc.tolerance formatted_tolerance = f"{tolerance:.{max(0, -int(f'{tolerance:.1e}'.split('e')[-1])) if tolerance < 1 else 1}f}" + if self.use_regex: + return self._execute_regex(context, key) + if self.calculate_sum: self._calculate_sum(context, key) else: @@ -1547,6 +1580,45 @@ class SelectSimilar(Operator): return None return ifcopenshell.util.selector.get_element_value(element, key) + def _execute_regex(self, context, key): + try: + pattern = re.compile(self.regex_pattern) + except re.error as e: + self.report({"ERROR"}, f"Invalid regular expression: {e}") + return {"CANCELLED"} + + count = 0 + if self.regex_mode == "FILTER": + for obj in context.selected_objects: + obj_value = self._get_value(obj, key) + if obj_value is not None and pattern.search(str(obj_value)): + count += 1 + else: + obj.select_set(False) + else: + remove = self.regex_mode == "REMOVE" + objects = context.scene.objects if self.should_unhide else context.visible_objects + for obj in objects: + obj_value = self._get_value(obj, key) + if obj_value is not None and pattern.search(str(obj_value)): + if self.should_unhide: + obj.hide_viewport = False + obj.hide_set(False) + obj.select_set(not remove) + count += 1 + + if self.regex_mode == "FILTER": + verb = "Filtered selection to" + elif self.regex_mode == "REMOVE": + verb = "Deselected" + else: + verb = "Selected" + clip_key = "PredefinedType" if key == "predefined_type" else key + result = f"{clip_key} = /.*{self.regex_pattern}.*/" + bpy.context.window_manager.clipboard = result + self.report({"INFO"}, f"{verb} {count} objects matching ({result}); query copied to the clipboard.") + return {"FINISHED"} + def _get_reference_values(self, context, key): objects = ( [context.active_object] diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index aac3c146bd..930d94f619 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import re from typing import TYPE_CHECKING import bpy @@ -221,7 +222,7 @@ class SelectType(bpy.types.Operator): class SelectSimilarType(bpy.types.Operator): - """Select Similar Type\nSHIFT+Click to remove from selection set\nCTRL+Click to filter selection to matching objects only\nALT+Click to also unhide hidden objects (viewport and local hide)""" + """Select Similar Type\nSHIFT+Click to remove from selection set\nCTRL+Click to filter selection to matching objects only\nCTRL+ALT+Click to search type names by regex in a dialog\nALT+Click to also unhide hidden objects (viewport and local hide)""" bl_idname = "bim.select_similar_type" bl_label = "Select Similar Type" @@ -230,15 +231,93 @@ class SelectSimilarType(bpy.types.Operator): should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) remove_from_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) filter_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + use_regex: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + regex_pattern: bpy.props.StringProperty( + name="Pattern", + description='Python regular expression matched anywhere in each type\'s name, e.g. "foo" matches ".*foo.*"', + ) + regex_mode: bpy.props.EnumProperty( + name="Action", + items=[ + ("ADD", "Add to Selection", "Select occurrences whose type name matches the pattern"), + ("REMOVE", "Remove from Selection", "Deselect occurrences whose type name matches the pattern"), + ("FILTER", "Filter Selection", "Keep only already selected occurrences whose type name matches the pattern"), + ], + default="ADD", + ) def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.ctrl and event.alt and not event.shift: + self.use_regex = True + if context.active_object and (element := tool.Ifc.get_entity(context.active_object)): + relating_type = ifcopenshell.util.element.get_type(element) + if relating_type and relating_type.Name: + self.regex_pattern = relating_type.Name + return context.window_manager.invoke_props_dialog(self) self.should_unhide = event.alt self.remove_from_selection = event.shift and not event.ctrl self.filter_selection = event.ctrl and not event.shift return self.execute(context) + def draw(self, context): + layout = self.layout + if not self.use_regex: + return + layout.prop(self, "regex_pattern") + layout.prop(self, "regex_mode") + layout.prop(self, "should_unhide", text="Also Unhide Hidden Objects") + + def _get_type_name(self, obj): + element = tool.Ifc.get_entity(obj) + if not element: + return None + relating_type = ifcopenshell.util.element.get_type(element) + if not relating_type: + return None + return relating_type.Name + + def _execute_regex(self, context): + try: + pattern = re.compile(self.regex_pattern) + except re.error as e: + self.report({"ERROR"}, f"Invalid regular expression: {e}") + return {"CANCELLED"} + + count = 0 + if self.regex_mode == "FILTER": + for obj in context.selected_objects: + name = self._get_type_name(obj) + if name is not None and pattern.search(name): + count += 1 + else: + obj.select_set(False) + else: + remove = self.regex_mode == "REMOVE" + objects = context.scene.objects if self.should_unhide else context.visible_objects + for obj in objects: + name = self._get_type_name(obj) + if name is not None and pattern.search(name): + if self.should_unhide: + obj.hide_viewport = False + obj.hide_set(False) + obj.select_set(not remove) + count += 1 + + if self.regex_mode == "FILTER": + verb = "Filtered selection to" + elif self.regex_mode == "REMOVE": + verb = "Deselected" + else: + verb = "Selected" + result = f"type = /.*{self.regex_pattern}.*/" + bpy.context.window_manager.clipboard = result + self.report({"INFO"}, f"{verb} {count} objects matching ({result}); query copied to the clipboard.") + return {"FINISHED"} + def execute(self, context): self.file = tool.Ifc.get() + if self.use_regex: + return self._execute_regex(context) if self.remove_from_selection or self.filter_selection: objects = [context.active_object] if context.active_object else [] else: