mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 18:43:26 +00:00
Replace select_products flags with a mode enum and consolidate loops
Spatial.select_products now takes mode="ADD"|"REMOVE"|"FILTER" instead of the mutually exclusive remove / filter_selection booleans, matching the regex dialog's action enum; helper.selection_mode() maps each operator's two flag properties to it. core.select_by_material updated accordingly. Upstream callers pass only products/unhide and are unaffected. select_ifc_class, select_similar_type, select_aggregate and select_linked_aggregates now collect elements and delegate to select_products instead of hand-rolling unhide/select/filter loops; select_aggregate's manual IsDecomposedBy recursion is replaced with get_parts/get_decomposition. Only select_similar keeps a bespoke loop (tolerance-based value matching). Intentional behavior deltas (also in the dev note): remove mode uses plain select_set(False), so select_ifc_class no longer re-anchors the active object; select_similar_type drops its O(n^2) visible_objects gate and can latently select hidden occurrences like the other operators; select_ifc_class and select_similar_type write the clipboard query in every mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,8 @@ All of the scheme's cross-operator plumbing lives in `bonsai/bim/helper.py`:
|
||||
`bl_description` / `description()`, ending the casing drift; `SelectIfcClass` and
|
||||
`SelectSimilarType` switched from class docstrings to `bl_description` to allow
|
||||
composition.
|
||||
- `selection_mode(remove, filter)` — maps the two operator flags to the
|
||||
`select_products` mode enum (`"ADD"|"REMOVE"|"FILTER"`).
|
||||
- `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
|
||||
@@ -69,10 +71,19 @@ All of the scheme's cross-operator plumbing lives in `bonsai/bim/helper.py`:
|
||||
clicked UI item (material, group, container row) are unaffected by this rule.
|
||||
- **Filter mode selects nothing new.** It computes the matched set and deselects
|
||||
already-selected objects outside it. Implemented centrally in
|
||||
`Spatial.select_products(products, unhide=..., remove=..., filter_selection=...)`
|
||||
(`tool/spatial.py`) for the UI-item operators, and as small per-operator branches
|
||||
where selection is done with bespoke loops (`select_similar`, `select_ifc_class`,
|
||||
`select_similar_type`, the two aggregate operators).
|
||||
`Spatial.select_products(products, unhide=..., mode="ADD"|"REMOVE"|"FILTER")`
|
||||
(`tool/spatial.py`); every operator except `select_similar` (whose per-value
|
||||
tolerance matching stays bespoke) now collects elements and delegates to it,
|
||||
computing the mode from its two flags via `helper.selection_mode()`.
|
||||
- **Consolidation trade-offs** (routing `select_ifc_class`, `select_similar_type`
|
||||
and the two aggregate operators through `select_products`): remove mode uses a
|
||||
plain `select_set(False)` — `select_ifc_class` no longer re-anchors the active
|
||||
object via `tool.Blender.deselect_object`; `select_similar_type` lost its
|
||||
O(n²) `obj in context.visible_objects` gate, so like the other operators it may
|
||||
latently select hidden occurrences (they appear selected when unhidden); the
|
||||
aggregate parts walk uses `get_parts`/`get_decomposition` instead of a manual
|
||||
`IsDecomposedBy` recursion; `select_ifc_class` and `select_similar_type` now
|
||||
write the clipboard query in every mode (previously skipped in filter mode).
|
||||
- **CTRL = filter, CTRL+SHIFT = legacy CTRL function.** Originally implemented the
|
||||
other way around; swapped after review because the two selection-set operations
|
||||
(subtract, intersect) belong on the simple modifiers. The demoted plain-CTRL
|
||||
|
||||
@@ -90,6 +90,15 @@ def decode_select_click(event: bpy.types.Event) -> SelectClickModifiers:
|
||||
)
|
||||
|
||||
|
||||
def selection_mode(remove_from_selection: bool, filter_selection: bool) -> str:
|
||||
"""Map the two modifier flags to a Spatial.select_products mode."""
|
||||
if remove_from_selection:
|
||||
return "REMOVE"
|
||||
if filter_selection:
|
||||
return "FILTER"
|
||||
return "ADD"
|
||||
|
||||
|
||||
class RegexSelectMixin:
|
||||
"""Scaffold for select operators offering the CTRL+ALT+Click regex-search dialog.
|
||||
|
||||
@@ -193,12 +202,7 @@ class RegexSelectMixin:
|
||||
|
||||
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",
|
||||
)
|
||||
tool.Spatial.select_products(products, unhide=self.should_unhide, mode=self.regex_mode)
|
||||
|
||||
|
||||
def draw_attributes(
|
||||
|
||||
@@ -34,6 +34,7 @@ from bonsai.bim.helper import (
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
selection_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -362,57 +363,24 @@ class BIM_OT_select_aggregate(RegexSelectMixin, bpy.types.Operator):
|
||||
|
||||
all_parts = list(aggregates.values())
|
||||
|
||||
products = set(all_parts)
|
||||
if self.select_parts:
|
||||
selected_parts = []
|
||||
for aggregate in all_parts:
|
||||
if self.one_level_deep:
|
||||
products.update(ifcopenshell.util.element.get_parts(aggregate))
|
||||
else:
|
||||
products.update(ifcopenshell.util.element.get_decomposition(aggregate))
|
||||
|
||||
for part in all_parts:
|
||||
if part.IsDecomposedBy:
|
||||
for rel in part.IsDecomposedBy:
|
||||
for subpart in rel.RelatedObjects:
|
||||
selected_parts.append(subpart)
|
||||
tool.Spatial.select_products(
|
||||
products,
|
||||
unhide=self.should_unhide,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
|
||||
# If not limited to one level, traverse deeper
|
||||
if not self.one_level_deep:
|
||||
|
||||
def add_descendants(elem):
|
||||
if elem.IsDecomposedBy:
|
||||
for rel in elem.IsDecomposedBy:
|
||||
for deeper in rel.RelatedObjects:
|
||||
selected_parts.append(deeper)
|
||||
add_descendants(deeper)
|
||||
|
||||
add_descendants(subpart)
|
||||
|
||||
if self.filter_selection:
|
||||
matched_objs = {tool.Ifc.get_object(element) for element in set(selected_parts + all_parts)}
|
||||
for obj in context.selected_objects:
|
||||
if obj not in matched_objs:
|
||||
obj.select_set(False)
|
||||
else:
|
||||
for element in set(selected_parts + all_parts):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
if self.should_unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
obj.select_set(not self.remove_from_selection)
|
||||
|
||||
else:
|
||||
if self.filter_selection:
|
||||
matched_objs = {tool.Ifc.get_object(element) for element in all_parts}
|
||||
for obj in context.selected_objects:
|
||||
if obj not in matched_objs:
|
||||
obj.select_set(False)
|
||||
else:
|
||||
for aggregate_element in all_parts:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate_element)
|
||||
if aggregate_obj:
|
||||
if self.should_unhide:
|
||||
aggregate_obj.hide_viewport = False
|
||||
aggregate_obj.hide_set(False)
|
||||
aggregate_obj.select_set(not self.remove_from_selection)
|
||||
if not self.remove_from_selection:
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
if not self.select_parts and not keep_current_selection:
|
||||
for aggregate_element in all_parts:
|
||||
if aggregate_obj := tool.Ifc.get_object(aggregate_element):
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
|
||||
# copy selection query to clipboard
|
||||
result = ""
|
||||
@@ -521,7 +489,7 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
|
||||
objects = [context.active_object] if context.active_object else []
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
matched_objs = set()
|
||||
products = set()
|
||||
for obj in objects:
|
||||
if not keep_current_selection:
|
||||
obj.select_set(False)
|
||||
@@ -543,39 +511,17 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
|
||||
for group_link in group_rel:
|
||||
parts = list(group_link.RelatedObjects)
|
||||
if self.select_parts:
|
||||
parts_objs = []
|
||||
for part in parts:
|
||||
if part.IsDecomposedBy:
|
||||
for subpart in part.IsDecomposedBy[0].RelatedObjects:
|
||||
parts.append(subpart)
|
||||
parts_objs.append(part)
|
||||
products.update(parts)
|
||||
|
||||
for element in parts_objs:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
if self.filter_selection:
|
||||
matched_objs.add(obj)
|
||||
continue
|
||||
if self.should_unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
obj.select_set(not self.remove_from_selection)
|
||||
else:
|
||||
for element in parts:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
if self.filter_selection:
|
||||
matched_objs.add(obj)
|
||||
continue
|
||||
if self.should_unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
obj.select_set(not self.remove_from_selection)
|
||||
|
||||
if self.filter_selection:
|
||||
for obj in context.selected_objects:
|
||||
if obj not in matched_objs:
|
||||
obj.select_set(False)
|
||||
tool.Spatial.select_products(
|
||||
products,
|
||||
unhide=self.should_unhide,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from bonsai.bim.helper import (
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
selection_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -252,8 +253,7 @@ class SelectGroupElements(RegexSelectMixin, bpy.types.Operator):
|
||||
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,
|
||||
remove=self.remove_from_selection,
|
||||
filter_selection=self.filter_selection,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ from bonsai.bim.helper import (
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
selection_mode,
|
||||
)
|
||||
from bonsai.bim.module.model import slab, wall
|
||||
|
||||
@@ -158,14 +159,10 @@ class SelectByMaterial(RegexSelectMixin, bpy.types.Operator):
|
||||
if not materials:
|
||||
return {"FINISHED"}
|
||||
|
||||
mode = selection_mode(self.remove_from_selection, self.filter_selection)
|
||||
for mat in materials.values():
|
||||
core.select_by_material(
|
||||
tool.Material,
|
||||
tool.Spatial,
|
||||
material=mat,
|
||||
should_unhide=self.should_unhide,
|
||||
remove_from_selection=self.remove_from_selection,
|
||||
filter_selection=self.filter_selection,
|
||||
tool.Material, tool.Spatial, material=mat, should_unhide=self.should_unhide, mode=mode
|
||||
)
|
||||
|
||||
result = " + ".join(f'material = "{self._get_name(m)}"' for m in materials.values())
|
||||
|
||||
@@ -45,6 +45,7 @@ from bonsai.bim.helper import (
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
selection_mode,
|
||||
)
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.prop import StrProperty
|
||||
@@ -1304,12 +1305,8 @@ class SelectIfcClass(Operator):
|
||||
if element := tool.Ifc.get_entity(obj):
|
||||
classes.add(element.is_a())
|
||||
predefined_types.add(ifcopenshell.util.element.get_predefined_type(element))
|
||||
if self.filter_selection:
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not any(element.is_a(cls) for cls in classes):
|
||||
obj.select_set(False)
|
||||
return {"FINISHED"}
|
||||
|
||||
elements = []
|
||||
for cls in classes:
|
||||
for element in tool.Ifc.get().by_type(cls):
|
||||
if (
|
||||
@@ -1317,14 +1314,12 @@ class SelectIfcClass(Operator):
|
||||
and ifcopenshell.util.element.get_predefined_type(element) not in predefined_types
|
||||
):
|
||||
continue
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
if self.should_unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
if self.remove_from_selection:
|
||||
tool.Blender.deselect_object(obj)
|
||||
else:
|
||||
tool.Blender.select_object(obj)
|
||||
elements.append(element)
|
||||
tool.Spatial.select_products(
|
||||
elements,
|
||||
unhide=self.should_unhide,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
|
||||
# copy selection query to clipboard
|
||||
result = " + ".join(classes)
|
||||
|
||||
@@ -29,6 +29,7 @@ from bonsai.bim.helper import (
|
||||
SELECT_REMOVE_TOOLTIP,
|
||||
SELECT_UNHIDE_TOOLTIP,
|
||||
decode_select_click,
|
||||
selection_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -337,12 +338,12 @@ class SelectSimilarContainer(bpy.types.Operator):
|
||||
if not containers:
|
||||
return {"CANCELLED"}
|
||||
|
||||
mode = selection_mode(self.remove_from_selection, self.filter_selection)
|
||||
for container in containers.values():
|
||||
tool.Spatial.select_products(
|
||||
tool.Spatial.get_decomposed_elements(container, self.is_recursive),
|
||||
unhide=self.should_unhide,
|
||||
remove=self.remove_from_selection,
|
||||
filter_selection=self.filter_selection,
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
result = " + ".join(f'location = "{c.Name}"' for c in containers.values())
|
||||
@@ -499,8 +500,7 @@ class SelectDecomposedElements(bpy.types.Operator):
|
||||
tool.Spatial.select_products(
|
||||
tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive),
|
||||
unhide=self.should_unhide,
|
||||
remove=self.remove_from_selection,
|
||||
filter_selection=self.filter_selection,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
|
||||
# Make selected active element in list, the active object
|
||||
|
||||
@@ -36,6 +36,7 @@ from bonsai.bim.helper import (
|
||||
RegexSelectMixin,
|
||||
decode_select_click,
|
||||
select_regex_tooltip,
|
||||
selection_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -293,32 +294,27 @@ class SelectSimilarType(RegexSelectMixin, bpy.types.Operator):
|
||||
continue
|
||||
relating_types.add(relating_type)
|
||||
|
||||
if self.filter_selection:
|
||||
for obj in context.selected_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or ifcopenshell.util.element.get_type(element) not in relating_types:
|
||||
obj.select_set(False)
|
||||
return {"FINISHED"}
|
||||
|
||||
elements = []
|
||||
result = ""
|
||||
for relating_type in relating_types:
|
||||
related_objects = ifcopenshell.util.element.get_types(relating_type)
|
||||
elements.extend(related_objects)
|
||||
|
||||
for element in related_objects:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj and (self.should_unhide or obj in context.visible_objects):
|
||||
if self.should_unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
obj.select_set(not self.remove_from_selection)
|
||||
|
||||
# copy selection query to clipboard
|
||||
# build selection query for the clipboard
|
||||
related_objects_class = related_objects[0].is_a()
|
||||
relating_type_name = relating_type.Name
|
||||
if not result:
|
||||
result = f'{related_objects_class}, type="{relating_type_name}"'
|
||||
else:
|
||||
result += f' + {related_objects_class}, type="{relating_type_name}"'
|
||||
|
||||
tool.Spatial.select_products(
|
||||
elements,
|
||||
unhide=self.should_unhide,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
|
||||
if result:
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
|
||||
|
||||
@@ -86,15 +86,9 @@ def select_by_material(
|
||||
spatial: type[tool.Spatial],
|
||||
material: ifcopenshell.entity_instance,
|
||||
should_unhide: bool = False,
|
||||
remove_from_selection: bool = False,
|
||||
filter_selection: bool = False,
|
||||
mode: str = "ADD",
|
||||
) -> None:
|
||||
spatial.select_products(
|
||||
material_tool.get_elements_by_material(material),
|
||||
unhide=should_unhide,
|
||||
remove=remove_from_selection,
|
||||
filter_selection=filter_selection,
|
||||
)
|
||||
spatial.select_products(material_tool.get_elements_by_material(material), unhide=should_unhide, mode=mode)
|
||||
|
||||
|
||||
def enable_editing_material(material_tool: type[tool.Material], material: ifcopenshell.entity_instance) -> None:
|
||||
|
||||
@@ -975,7 +975,7 @@ class Spatial:
|
||||
def run_spatial_assign_container(cls, container, objs): pass
|
||||
def run_spatial_import_spatial_decomposition(cls): pass
|
||||
def select_object(cls, obj): pass
|
||||
def select_products(cls, products, unhide=False, remove=False, filter_selection=False): pass
|
||||
def select_products(cls, products, unhide=False, mode="ADD"): pass
|
||||
def set_active_object(cls, obj, selection_mode=None): pass
|
||||
def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass
|
||||
def set_target_container_as_default(cls): pass
|
||||
|
||||
@@ -196,13 +196,12 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
cls,
|
||||
products: Iterable[ifcopenshell.entity_instance],
|
||||
unhide: bool = False,
|
||||
remove: bool = False,
|
||||
filter_selection: bool = False,
|
||||
mode: Literal["ADD", "REMOVE", "FILTER"] = "ADD",
|
||||
) -> None:
|
||||
assert (view_layer := bpy.context.view_layer)
|
||||
# Update view layer, otherwise `objects` might be missing just created objects.
|
||||
view_layer.update()
|
||||
if filter_selection:
|
||||
if mode == "FILTER":
|
||||
# Keep only the already selected objects that match, select nothing new.
|
||||
matched_objs = set()
|
||||
for product in products:
|
||||
@@ -219,7 +218,7 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
if unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
obj.select_set(not remove)
|
||||
obj.select_set(mode != "REMOVE")
|
||||
|
||||
@classmethod
|
||||
def filter_products(
|
||||
|
||||
Reference in New Issue
Block a user