mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 02:47:48 +00:00
Refactor select operators onto shared scaffolding in bim.helper
- RegexSelectMixin owns the CTRL+ALT+Click regex dialog (properties, draw, pattern compilation, verb/clipboard/report tail) with two strategies: apply_regex_by_value for per-object matching and select_regex_products for union-of-products operators. The five regex operators now implement only apply_regex, get_regex_prefill and their clipboard key / count noun. - decode_select_click(event) centralizes the modifier scheme (unhide / remove / filter / legacy / regex_dialog); all nine select operators use it. Per-operator LEFTMOUSE type guards were dropped, so keyboard invocation now honors modifiers uniformly. - Tooltips compose from shared constants, fixing casing drift; SelectIfcClass and SelectSimilarType switch from docstrings to bl_description to allow composition. - Delete core.select_similar_container, dead since the #7940 merge made SelectSimilarContainer call Spatial.select_products directly. No behavior changes besides the keyboard-invocation and tooltip-text normalization noted above. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,31 @@ Operators covered: `bim.select_similar`, `bim.select_ifc_class`,
|
||||
`bim.select_decomposed_elements`, `bim.select_group_elements`,
|
||||
`bim.select_aggregate`, `bim.select_linked_aggregates`.
|
||||
|
||||
### Shared scaffolding (`bonsai.bim.helper`)
|
||||
|
||||
All of the scheme's cross-operator plumbing lives in `bonsai/bim/helper.py`:
|
||||
|
||||
- `decode_select_click(event)` → `SelectClickModifiers` named tuple (`unhide`,
|
||||
`remove`, `filter`, `legacy`, `regex_dialog`) — the single place the modifier
|
||||
scheme is defined; all nine `invoke`s use it. The per-operator
|
||||
`event.type == "LEFTMOUSE"` guards were dropped in the process, so keyboard
|
||||
invocation now honors modifiers uniformly.
|
||||
- `SELECT_REMOVE_TOOLTIP` / `SELECT_FILTER_TOOLTIP` / `SELECT_UNHIDE_TOOLTIP` +
|
||||
`select_regex_tooltip(subject)` — tooltip lines composed into every
|
||||
`bl_description` / `description()`, ending the casing drift; `SelectIfcClass` and
|
||||
`SelectSimilarType` switched from class docstrings to `bl_description` to allow
|
||||
composition.
|
||||
- `RegexSelectMixin` — the whole regex-dialog scaffold (properties, `draw()`,
|
||||
compile-with-error-handling, verb/clipboard/report tail) plus two reusable
|
||||
strategies: `apply_regex_by_value(context, pattern, get_value)` for per-object
|
||||
matching and `select_regex_products(products)` for union-of-products operators.
|
||||
Subclasses implement `apply_regex` + `get_regex_prefill` and set
|
||||
`regex_clipboard_key` / `regex_count_noun`; `draw_regex_options` is the hook for
|
||||
extra dialog rows (used by `select_aggregate`).
|
||||
|
||||
`core.select_similar_container` was deleted — dead since the #7940 merge made
|
||||
`SelectSimilarContainer` loop `Spatial.select_products` directly.
|
||||
|
||||
### Key decisions and the why
|
||||
|
||||
- **Remove/filter criteria come from the active object only** (not all selected
|
||||
|
||||
@@ -20,9 +20,10 @@ from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from types import EllipsisType
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Union
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
@@ -56,6 +57,150 @@ if TYPE_CHECKING:
|
||||
ExportCallback = Callable[[dict[str, Any], bonsai.bim.prop.Attribute], bool]
|
||||
|
||||
|
||||
# Shared scaffolding for the select-operator modifier scheme
|
||||
# (see docs/dev-notes/Additional_Selection_and_Deselection_Tools.md):
|
||||
# Click = select, SHIFT = remove from selection, CTRL = filter selection,
|
||||
# CTRL+SHIFT = legacy CTRL function (one-level-deep / exclude-children /
|
||||
# calculate-sum), ALT = also unhide, CTRL+ALT = regex-search dialog.
|
||||
|
||||
SELECT_REMOVE_TOOLTIP = "SHIFT+Click to remove from selection set"
|
||||
SELECT_FILTER_TOOLTIP = "CTRL+Click to filter selection to matching objects only"
|
||||
SELECT_UNHIDE_TOOLTIP = "ALT+Click to also unhide hidden objects (viewport and local hide)"
|
||||
|
||||
|
||||
def select_regex_tooltip(subject: str = "values") -> str:
|
||||
return f"CTRL+ALT+Click to search {subject} by regex in a dialog"
|
||||
|
||||
|
||||
class SelectClickModifiers(NamedTuple):
|
||||
unhide: bool
|
||||
remove: bool
|
||||
filter: bool
|
||||
legacy: bool # CTRL+SHIFT: one-level-deep / exclude-children / calculate-sum
|
||||
regex_dialog: bool # CTRL+ALT
|
||||
|
||||
|
||||
def decode_select_click(event: bpy.types.Event) -> SelectClickModifiers:
|
||||
return SelectClickModifiers(
|
||||
unhide=event.alt,
|
||||
remove=event.shift and not event.ctrl,
|
||||
filter=event.ctrl and not event.shift,
|
||||
legacy=event.ctrl and event.shift,
|
||||
regex_dialog=event.ctrl and event.alt and not event.shift,
|
||||
)
|
||||
|
||||
|
||||
class RegexSelectMixin:
|
||||
"""Scaffold for select operators offering the CTRL+ALT+Click regex-search dialog.
|
||||
|
||||
Subclasses must be Operators that also define a ``should_unhide`` BoolProperty,
|
||||
implement :meth:`apply_regex`, and override :attr:`regex_clipboard_key` /
|
||||
:attr:`regex_count_noun` as needed. From ``invoke``, return
|
||||
:meth:`invoke_regex_dialog` when ``decode_select_click(event).regex_dialog`` is
|
||||
set; from ``execute``, return :meth:`execute_regex` while ``use_regex`` is on.
|
||||
"""
|
||||
|
||||
use_regex: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
regex_pattern: bpy.props.StringProperty(
|
||||
name="Pattern",
|
||||
description='Python regular expression matched anywhere in the 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",
|
||||
)
|
||||
|
||||
regex_clipboard_key = "Name"
|
||||
regex_count_noun = "objects"
|
||||
|
||||
def get_regex_prefill(self, context: bpy.types.Context) -> Union[str, None]:
|
||||
"""Initial pattern shown in the dialog; None keeps the previous pattern."""
|
||||
return None
|
||||
|
||||
def draw_regex_options(self, context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
||||
"""Hook for extra dialog rows between the action dropdown and the unhide toggle."""
|
||||
|
||||
def get_regex_clipboard_key(self) -> str:
|
||||
return self.regex_clipboard_key
|
||||
|
||||
def invoke_regex_dialog(self, context: bpy.types.Context):
|
||||
self.use_regex = True
|
||||
prefill = self.get_regex_prefill(context)
|
||||
if prefill is not None:
|
||||
self.regex_pattern = prefill
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
if not self.use_regex:
|
||||
return
|
||||
layout = self.layout
|
||||
layout.prop(self, "regex_pattern")
|
||||
layout.prop(self, "regex_mode")
|
||||
self.draw_regex_options(context, layout)
|
||||
layout.prop(self, "should_unhide", text="Also Unhide Hidden Objects")
|
||||
|
||||
def execute_regex(self, context: bpy.types.Context):
|
||||
try:
|
||||
pattern = re.compile(self.regex_pattern)
|
||||
except re.error as e:
|
||||
self.report({"ERROR"}, f"Invalid regular expression: {e}")
|
||||
return {"CANCELLED"}
|
||||
count = self.apply_regex(context, pattern)
|
||||
verb = {"ADD": "Selected", "REMOVE": "Deselected", "FILTER": "Filtered selection to"}[self.regex_mode]
|
||||
result = f"{self.get_regex_clipboard_key()} = /.*{self.regex_pattern}.*/"
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report(
|
||||
{"INFO"}, f"{verb} {count} {self.regex_count_noun} matching ({result}); query copied to the clipboard."
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def apply_regex(self, context: bpy.types.Context, pattern: re.Pattern) -> int:
|
||||
"""Apply the pattern per regex_mode; return the matched count."""
|
||||
raise NotImplementedError
|
||||
|
||||
def apply_regex_by_value(
|
||||
self,
|
||||
context: bpy.types.Context,
|
||||
pattern: re.Pattern,
|
||||
get_value: Callable[[bpy.types.Object], Union[str, None]],
|
||||
) -> int:
|
||||
"""Generic per-object matching against get_value(obj)."""
|
||||
count = 0
|
||||
if self.regex_mode == "FILTER":
|
||||
for obj in context.selected_objects:
|
||||
value = get_value(obj)
|
||||
if value is not None and pattern.search(value):
|
||||
count += 1
|
||||
else:
|
||||
obj.select_set(False)
|
||||
return count
|
||||
remove = self.regex_mode == "REMOVE"
|
||||
objects = context.scene.objects if self.should_unhide else context.visible_objects
|
||||
for obj in objects:
|
||||
value = get_value(obj)
|
||||
if value is not None and pattern.search(value):
|
||||
if self.should_unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
obj.select_set(not remove)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def select_regex_products(self, products: Iterable[ifcopenshell.entity_instance]) -> None:
|
||||
"""Apply regex_mode + unhide to IFC products via Spatial.select_products."""
|
||||
tool.Spatial.select_products(
|
||||
products,
|
||||
unhide=self.should_unhide,
|
||||
remove=self.regex_mode == "REMOVE",
|
||||
filter_selection=self.regex_mode == "FILTER",
|
||||
)
|
||||
|
||||
|
||||
def draw_attributes(
|
||||
props: Union[bpy.types.bpy_prop_collection_idprop[Attribute], Sequence[Attribute]],
|
||||
layout: bpy.types.UILayout,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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
|
||||
@@ -28,6 +27,14 @@ import ifcopenshell.util.element
|
||||
import bonsai.core.aggregate as core
|
||||
import bonsai.core.spatial
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import (
|
||||
SELECT_FILTER_TOOLTIP,
|
||||
SELECT_REMOVE_TOOLTIP,
|
||||
SELECT_UNHIDE_TOOLTIP,
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
)
|
||||
|
||||
|
||||
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -255,7 +262,7 @@ class BIM_OT_select_parts(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
class BIM_OT_select_aggregate(RegexSelectMixin, bpy.types.Operator):
|
||||
"""Select Aggregate"""
|
||||
|
||||
bl_idname = "bim.select_aggregate"
|
||||
@@ -270,61 +277,46 @@ 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",
|
||||
)
|
||||
|
||||
regex_clipboard_key = "parent"
|
||||
regex_count_noun = "aggregates"
|
||||
|
||||
@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\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\nCTRL+ALT+Click to search aggregate names by regex in a dialog\nALT+Click to also unhide hidden objects (viewport and local hide)"
|
||||
base = "Select Aggregate and Parts." if properties.select_parts else "Select Aggregate"
|
||||
one_level_deep = "\nCTRL+SHIFT+Click to select only one level deep" if properties.select_parts else ""
|
||||
return (
|
||||
base
|
||||
+ f"\n\n{SELECT_REMOVE_TOOLTIP}"
|
||||
+ f"\n{SELECT_FILTER_TOOLTIP}"
|
||||
+ one_level_deep
|
||||
+ f"\n{select_regex_tooltip('aggregate names')}"
|
||||
+ f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
)
|
||||
|
||||
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
|
||||
self.remove_from_selection = event.shift and not event.ctrl
|
||||
self.filter_selection = event.ctrl and not event.shift
|
||||
mods = decode_select_click(event)
|
||||
if mods.regex_dialog:
|
||||
return self.invoke_regex_dialog(context)
|
||||
self.one_level_deep = mods.legacy
|
||||
self.should_unhide = mods.unhide
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
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")
|
||||
def get_regex_prefill(self, context):
|
||||
if context.active_object and (element := tool.Ifc.get_entity(context.active_object)):
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
return aggregate.Name
|
||||
return None
|
||||
|
||||
def draw_regex_options(self, context, layout):
|
||||
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"}
|
||||
|
||||
def apply_regex(self, context, pattern):
|
||||
aggregates = {}
|
||||
for rel in tool.Ifc.get().by_type("IfcRelAggregates"):
|
||||
aggregate = rel.RelatingObject
|
||||
@@ -343,30 +335,12 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
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"}
|
||||
self.select_regex_products(products)
|
||||
return len(aggregates)
|
||||
|
||||
def execute(self, context):
|
||||
if self.use_regex:
|
||||
return self._execute_regex(context)
|
||||
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 []
|
||||
@@ -527,15 +501,18 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.select_parts:
|
||||
return "Select all aggregates, subaggregates and all their parts\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)"
|
||||
else:
|
||||
return "Select all aggregates\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)"
|
||||
base = (
|
||||
"Select all aggregates, subaggregates and all their parts"
|
||||
if properties.select_parts
|
||||
else "Select all aggregates"
|
||||
)
|
||||
return base + f"\n\n{SELECT_REMOVE_TOOLTIP}" + f"\n{SELECT_FILTER_TOOLTIP}" + f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.should_unhide = event.alt
|
||||
self.remove_from_selection = event.shift and not event.ctrl
|
||||
self.filter_selection = event.ctrl and not event.shift
|
||||
mods = decode_select_click(event)
|
||||
self.should_unhide = mods.unhide
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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
|
||||
@@ -25,6 +24,14 @@ import ifcopenshell.util.element
|
||||
|
||||
import bonsai.bim.helper
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import (
|
||||
SELECT_FILTER_TOOLTIP,
|
||||
SELECT_REMOVE_TOOLTIP,
|
||||
SELECT_UNHIDE_TOOLTIP,
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
)
|
||||
|
||||
|
||||
class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -192,65 +199,43 @@ class UnassignGroup(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.report({"INFO"}, f"Unassigned {len(products)} objects from group.")
|
||||
|
||||
|
||||
class SelectGroupElements(bpy.types.Operator):
|
||||
class SelectGroupElements(RegexSelectMixin, bpy.types.Operator):
|
||||
bl_idname = "bim.select_group_elements"
|
||||
bl_label = "Select Group elements"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_description = (
|
||||
"Select objects assigned to the selected group and all nested groups"
|
||||
"\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)"
|
||||
+ f"\n{SELECT_REMOVE_TOOLTIP}"
|
||||
+ f"\n{SELECT_FILTER_TOOLTIP}"
|
||||
+ "\nCTRL+SHIFT+Click to exclude children"
|
||||
+ f"\n{select_regex_tooltip('group names')}"
|
||||
+ f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
)
|
||||
group: bpy.props.IntProperty()
|
||||
is_recursive: bpy.props.BoolProperty(name="Is Recursive", default=True, options={"SKIP_SAVE"})
|
||||
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",
|
||||
)
|
||||
|
||||
regex_clipboard_key = "group"
|
||||
regex_count_noun = "groups"
|
||||
|
||||
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
|
||||
mods = decode_select_click(event)
|
||||
if mods.regex_dialog:
|
||||
return self.invoke_regex_dialog(context)
|
||||
self.is_recursive = not mods.legacy
|
||||
self.should_unhide = mods.unhide
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
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"}
|
||||
def get_regex_prefill(self, context):
|
||||
if self.group:
|
||||
return tool.Ifc.get().by_id(self.group).Name
|
||||
return None
|
||||
|
||||
def apply_regex(self, context, pattern):
|
||||
products = set()
|
||||
matched_groups = 0
|
||||
for group in tool.Ifc.get().by_type("IfcGroup"):
|
||||
@@ -258,31 +243,12 @@ class SelectGroupElements(bpy.types.Operator):
|
||||
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"}
|
||||
self.select_regex_products(products)
|
||||
return matched_groups
|
||||
|
||||
def execute(self, context):
|
||||
if self.use_regex:
|
||||
return self._execute_regex(context)
|
||||
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,7 +17,6 @@
|
||||
# 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
|
||||
@@ -31,6 +30,14 @@ import bonsai.bim.helper
|
||||
import bonsai.bim.module.model.profile as model_profile
|
||||
import bonsai.core.material as core
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import (
|
||||
SELECT_FILTER_TOOLTIP,
|
||||
SELECT_REMOVE_TOOLTIP,
|
||||
SELECT_UNHIDE_TOOLTIP,
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
)
|
||||
from bonsai.bim.module.model import slab, wall
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -59,56 +66,45 @@ class DisableEditingMaterials(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectByMaterial(bpy.types.Operator):
|
||||
class SelectByMaterial(RegexSelectMixin, 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\nCTRL+ALT+Click to search material names by regex in a dialog\nALT+Click to also unhide hidden objects (viewport and local hide)"
|
||||
bl_description = (
|
||||
"Select objects using the provided material"
|
||||
+ f"\n\n{SELECT_REMOVE_TOOLTIP}"
|
||||
+ f"\n{SELECT_FILTER_TOOLTIP}"
|
||||
+ f"\n{select_regex_tooltip('material names')}"
|
||||
+ f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
)
|
||||
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",
|
||||
)
|
||||
|
||||
regex_clipboard_key = "material"
|
||||
|
||||
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
|
||||
mods = decode_select_click(event)
|
||||
if mods.regex_dialog:
|
||||
return self.invoke_regex_dialog(context)
|
||||
self.should_unhide = mods.unhide
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
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_regex_prefill(self, context):
|
||||
name = None
|
||||
if context.active_object:
|
||||
name = self._get_material_name(context.active_object, self._get_reference_layer_index())
|
||||
if name is None and self.material:
|
||||
name = self._get_name(tool.Ifc.get().by_id(self.material))
|
||||
return name
|
||||
|
||||
def _get_reference_layer_index(self):
|
||||
if not self.material:
|
||||
return None
|
||||
return self._get_layer_index(tool.Ifc.get().by_id(self.material))
|
||||
|
||||
def _get_material_name(self, obj, layer_index):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -122,51 +118,13 @@ class SelectByMaterial(bpy.types.Operator):
|
||||
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 apply_regex(self, context, pattern):
|
||||
layer_index = self._get_reference_layer_index()
|
||||
return self.apply_regex_by_value(context, pattern, lambda obj: self._get_material_name(obj, layer_index))
|
||||
|
||||
def execute(self, context):
|
||||
if self.use_regex:
|
||||
return self._execute_regex(context)
|
||||
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,7 +18,6 @@
|
||||
|
||||
import bisect
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Any, Literal, assert_never, get_args
|
||||
|
||||
@@ -39,6 +38,14 @@ from natsort import natsorted
|
||||
|
||||
import bonsai.core.search as core
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import (
|
||||
SELECT_FILTER_TOOLTIP,
|
||||
SELECT_REMOVE_TOOLTIP,
|
||||
SELECT_UNHIDE_TOOLTIP,
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
)
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.prop import StrProperty
|
||||
|
||||
@@ -1265,10 +1272,14 @@ class SelectGlobalId(Operator):
|
||||
|
||||
|
||||
class SelectIfcClass(Operator):
|
||||
"""Click to select all objects that match with the given IFC class\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_idname = "bim.select_ifc_class"
|
||||
bl_label = "Select IFC Class"
|
||||
bl_description = (
|
||||
"Click to select all objects that match with the given IFC class"
|
||||
+ f"\n{SELECT_REMOVE_TOOLTIP}"
|
||||
+ f"\n{SELECT_FILTER_TOOLTIP}"
|
||||
+ f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
should_filter_predefined_type: BoolProperty(default=False)
|
||||
should_unhide: BoolProperty(default=False)
|
||||
@@ -1276,9 +1287,10 @@ class SelectIfcClass(Operator):
|
||||
filter_selection: BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.remove_from_selection = event.shift and not event.ctrl
|
||||
self.filter_selection = event.ctrl and not event.shift
|
||||
self.should_unhide = event.alt
|
||||
mods = decode_select_click(event)
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
self.should_unhide = mods.unhide
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
@@ -1457,7 +1469,7 @@ class ShowAllElements(Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SelectSimilar(Operator):
|
||||
class SelectSimilar(RegexSelectMixin, Operator):
|
||||
bl_idname = "bim.select_similar"
|
||||
bl_label = "Select Similar"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
@@ -1470,24 +1482,16 @@ 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.\nCTRL+ALT+CLICK search by regex in a dialog.\nALT+CLICK also unhide hidden objects (viewport and local hide)."
|
||||
base = (
|
||||
"Select objects with a similar value"
|
||||
+ f"\n\n{SELECT_REMOVE_TOOLTIP}"
|
||||
+ f"\n{SELECT_FILTER_TOOLTIP}"
|
||||
+ f"\n{select_regex_tooltip()}"
|
||||
+ f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
)
|
||||
|
||||
key = getattr(properties, "key", None)
|
||||
active = context.active_object
|
||||
@@ -1500,7 +1504,7 @@ class SelectSimilar(Operator):
|
||||
|
||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||
if isinstance(value, (int, float)):
|
||||
return base + ("\nCTRL+SHIFT+CLICK display the sum of all selected objects")
|
||||
return base + ("\nCTRL+SHIFT+Click to display the sum of all selected objects")
|
||||
else:
|
||||
return base
|
||||
|
||||
@@ -1512,26 +1516,33 @@ 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
|
||||
mods = decode_select_click(event)
|
||||
if mods.regex_dialog:
|
||||
return self.invoke_regex_dialog(context)
|
||||
self.calculate_sum = mods.legacy
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
self.should_unhide = mods.unhide
|
||||
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_regex_prefill(self, context):
|
||||
if not context.active_object:
|
||||
return None
|
||||
key = "predefined_type" if self.key == "PredefinedType" else self.key
|
||||
value = self._get_value(context.active_object, key)
|
||||
return None if value is None else str(value)
|
||||
|
||||
def get_regex_clipboard_key(self):
|
||||
return self.key
|
||||
|
||||
def apply_regex(self, context, pattern):
|
||||
key = "predefined_type" if self.key == "PredefinedType" else self.key
|
||||
|
||||
def get_value(obj):
|
||||
value = self._get_value(obj, key)
|
||||
return None if value is None else str(value)
|
||||
|
||||
return self.apply_regex_by_value(context, pattern, get_value)
|
||||
|
||||
def execute(self, context):
|
||||
self.calculated_sum = 0 # reset if run before
|
||||
@@ -1541,7 +1552,7 @@ class SelectSimilar(Operator):
|
||||
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)
|
||||
return self.execute_regex(context)
|
||||
|
||||
if self.calculate_sum:
|
||||
self._calculate_sum(context, key)
|
||||
@@ -1580,45 +1591,6 @@ 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]
|
||||
|
||||
@@ -24,6 +24,12 @@ import ifcopenshell.util.element
|
||||
import bonsai.bim.handler
|
||||
import bonsai.core.spatial as core
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import (
|
||||
SELECT_FILTER_TOOLTIP,
|
||||
SELECT_REMOVE_TOOLTIP,
|
||||
SELECT_UNHIDE_TOOLTIP,
|
||||
decode_select_click,
|
||||
)
|
||||
|
||||
|
||||
class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -284,7 +290,13 @@ class SelectContainer(bpy.types.Operator):
|
||||
class SelectSimilarContainer(bpy.types.Operator):
|
||||
bl_idname = "bim.select_similar_container"
|
||||
bl_label = "Select Similar Container"
|
||||
bl_description = "Recursively selects all objects in the container.\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)"
|
||||
bl_description = (
|
||||
"Recursively selects all objects in the container."
|
||||
+ f"\n\n{SELECT_REMOVE_TOOLTIP}"
|
||||
+ f"\n{SELECT_FILTER_TOOLTIP}"
|
||||
+ "\nCTRL+SHIFT+Click to select only one level deep"
|
||||
+ f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
container: bpy.props.IntProperty(default=0)
|
||||
@@ -294,11 +306,11 @@ class SelectSimilarContainer(bpy.types.Operator):
|
||||
filter_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.ctrl and event.shift:
|
||||
self.is_recursive = False
|
||||
self.should_unhide = event.alt
|
||||
self.remove_from_selection = event.shift and not event.ctrl
|
||||
self.filter_selection = event.ctrl and not event.shift
|
||||
mods = decode_select_click(event)
|
||||
self.is_recursive = not mods.legacy
|
||||
self.should_unhide = mods.unhide
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
@@ -469,18 +481,18 @@ class SelectDecomposedElements(bpy.types.Operator):
|
||||
def description(cls, context, operator):
|
||||
return (
|
||||
"Select the active item"
|
||||
+ "\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)"
|
||||
+ f"\n{SELECT_REMOVE_TOOLTIP}"
|
||||
+ f"\n{SELECT_FILTER_TOOLTIP}"
|
||||
+ "\nCTRL+SHIFT+Click to select only one level deep"
|
||||
+ f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE":
|
||||
if event.ctrl and event.shift:
|
||||
self.is_recursive = False
|
||||
self.should_unhide = event.alt
|
||||
self.remove_from_selection = event.shift and not event.ctrl
|
||||
self.filter_selection = event.ctrl and not event.shift
|
||||
mods = decode_select_click(event)
|
||||
self.is_recursive = not mods.legacy
|
||||
self.should_unhide = mods.unhide
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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
|
||||
@@ -30,6 +29,14 @@ import bonsai.core.geometry
|
||||
import bonsai.core.root
|
||||
import bonsai.core.type as core
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import (
|
||||
SELECT_FILTER_TOOLTIP,
|
||||
SELECT_REMOVE_TOOLTIP,
|
||||
SELECT_UNHIDE_TOOLTIP,
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
)
|
||||
|
||||
|
||||
class AssignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -221,51 +228,39 @@ class SelectType(bpy.types.Operator):
|
||||
return collection_in_view_layer
|
||||
|
||||
|
||||
class SelectSimilarType(bpy.types.Operator):
|
||||
"""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)"""
|
||||
|
||||
class SelectSimilarType(RegexSelectMixin, bpy.types.Operator):
|
||||
bl_idname = "bim.select_similar_type"
|
||||
bl_label = "Select Similar Type"
|
||||
bl_description = (
|
||||
"Select Similar Type"
|
||||
+ f"\n{SELECT_REMOVE_TOOLTIP}"
|
||||
+ f"\n{SELECT_FILTER_TOOLTIP}"
|
||||
+ f"\n{select_regex_tooltip('type names')}"
|
||||
+ f"\n{SELECT_UNHIDE_TOOLTIP}"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
related_object: bpy.props.StringProperty()
|
||||
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",
|
||||
)
|
||||
|
||||
regex_clipboard_key = "type"
|
||||
|
||||
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
|
||||
mods = decode_select_click(event)
|
||||
if mods.regex_dialog:
|
||||
return self.invoke_regex_dialog(context)
|
||||
self.should_unhide = mods.unhide
|
||||
self.remove_from_selection = mods.remove
|
||||
self.filter_selection = mods.filter
|
||||
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_regex_prefill(self, context):
|
||||
if context.active_object and (element := tool.Ifc.get_entity(context.active_object)):
|
||||
relating_type = ifcopenshell.util.element.get_type(element)
|
||||
if relating_type:
|
||||
return relating_type.Name
|
||||
return None
|
||||
|
||||
def _get_type_name(self, obj):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -276,48 +271,13 @@ class SelectSimilarType(bpy.types.Operator):
|
||||
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 apply_regex(self, context, pattern):
|
||||
return self.apply_regex_by_value(context, pattern, self._get_type_name)
|
||||
|
||||
def execute(self, context):
|
||||
self.file = tool.Ifc.get()
|
||||
if self.use_regex:
|
||||
return self._execute_regex(context)
|
||||
return self.execute_regex(context)
|
||||
if self.remove_from_selection or self.filter_selection:
|
||||
objects = [context.active_object] if context.active_object else []
|
||||
else:
|
||||
|
||||
@@ -119,22 +119,6 @@ def select_container(
|
||||
spatial.set_active_object(ifc.get_object(container), selection_mode=selection_mode)
|
||||
|
||||
|
||||
def select_similar_container(
|
||||
spatial: type[tool.Spatial],
|
||||
container: ifcopenshell.entity_instance,
|
||||
is_recursive: bool = True,
|
||||
should_unhide: bool = False,
|
||||
remove_from_selection: bool = False,
|
||||
filter_selection: bool = False,
|
||||
) -> None:
|
||||
spatial.select_products(
|
||||
spatial.get_decomposed_elements(container, is_recursive),
|
||||
unhide=should_unhide,
|
||||
remove=remove_from_selection,
|
||||
filter_selection=filter_selection,
|
||||
)
|
||||
|
||||
|
||||
def select_product(spatial: type[tool.Spatial], product: ifcopenshell.entity_instance) -> None:
|
||||
spatial.select_products([product])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user