mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Add CTRL+ALT+Click regex-search dialog to five select operators
Opens a props dialog prefilled with the active object's value (key value, type name, material name, group name, or aggregate name), then matches it as an unanchored Python regex (entering foo behaves like .*foo.*) with an Add / Remove / Filter action dropdown and an "Also Unhide Hidden Objects" checkbox. Applied to select_similar, select_similar_type, select_by_material, select_group_elements and select_aggregate. Multi-match operators (groups, aggregates) union their matches before a single Spatial.select_products call so Filter cannot wrongly intersect per-match. select_aggregate matches only IfcElement aggregates, excluding spatial decomposition, and exposes Also Select Parts / One Level Deep in the dialog. The equivalent selector query (e.g. material = /.*foo.*/) is copied to the clipboard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 []
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user