Files
IfcOpenShell/docs/dev-notes/Additional_Selection_and_Deselection_Tools.md
T
Ryan Schultz 03b3d3f627 Make Type Attributes panel values select_similar buttons
Each attribute value in BIM_PT_type_attributes is now a
bim.select_similar button with key "type.<Attribute>" (the selector
walks type. natively), mirroring the object Attributes panel pattern.
This gives type attributes the full modifier scheme - select, SHIFT
remove, CTRL filter, CTRL+SHIFT sum, ALT unhide, CTRL+ALT regex
dialog - with no operator changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 09:30:59 -05:00

185 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- This file was generated with the assistance of an AI coding tool. -->
# Additional selection and deselection tools — uniform modifier scheme for select operators
> **Living dev note** for the `Additional_Selection_and_Deselection_Tools` branch/PR.
> Read before working on the feature; append decisions and findings as the PR is
> refined. This is *not* user documentation — at merge it is removed or its durable
> parts promoted to code comments. See [README.md](README.md) for the convention.
## Problem
Bonsai's many "select …" buttons (by class, by type, by material, by container, by
group, by aggregate, by similar value) could only **add** to the selection. There was
no way to subtract matches from a large selection, or to narrow a selection down to
just the matches. `bim.select_similar` had grown a SHIFT+Click
`remove_from_selection`, but nothing else had it, and each operator had accumulated
its own ad-hoc modifier bindings.
## Design
One modifier scheme, applied uniformly across nine operators:
| Modifier | Action |
|---|---|
| Click | select matches (additive) |
| SHIFT+Click | **remove** matches from the selection set |
| 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`,
`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.
- `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
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
objects), following `SelectSimilar._get_reference_values`. If criteria came from
every selected object, a SHIFT/CTRL click would typically match — and wipe or keep —
the entire selection, which is useless. Plain Click keeps the old behavior
(criteria from all selected objects). Operators whose criteria come from the
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=..., 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
functions are: one-level-deep (`select_aggregate`, `select_similar_container`,
`select_decomposed_elements`), exclude-children (`select_group_elements`), and
calculate-sum (`select_similar`). Existing muscle memory for those will now hit
the filter instead — deliberate trade-off.
- **Modifiers resolve exclusively** in every `invoke`: SHIFT means remove only when
CTRL is up, CTRL means filter only when SHIFT is up, so combos are unambiguous.
- **Aggregate operators keep the current selection in remove/filter mode.** Their
normal flow deselects the seed selection before selecting targets; doing that in
remove/filter mode would destroy the very selection being edited
(`keep_current_selection` in `aggregate/operator.py`).
- **`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).
### Type Attributes panel hooks into the scheme
`BIM_PT_type_attributes` (type/ui.py) now renders each attribute value as a
`bim.select_similar` button with `key = "type.<Attribute>"` — the same
label-as-button pattern the object Attributes panel uses (attribute/ui.py). The
selector walks `type.` natively, so the full scheme (SHIFT/CTRL/CTRL+SHIFT
sum/ALT/CTRL+ALT regex) applies to type attributes with no operator changes.
### Deliberately overwritten SHIFT bindings (to be reworked later)
Two operators already used SHIFT; the owner chose to overwrite them and revisit with
another approach:
- `bim.select_ifc_class`: SHIFT used to mean "also match Predefined Type". The
`should_filter_predefined_type` property still exists but has **no key binding**.
- `bim.select_decomposed_elements`: SHIFT used to mean "select all listed elements"
(itself moved from ALT when ALT became unhide). `should_filter` still exists,
default True, **no key binding**.
## Status — implemented
Branched from `Unhide_with_alt_click` (ALT+Click unhide across the same operators,
plus container tools in the spatial decomposition panel). Commits so far:
- `3c6d08fabf` SHIFT+Click remove-from-selection (criteria from active object).
- `11be7e2b62` CTRL+Click filter-selection + the CTRL / CTRL+SHIFT swap.
Files: `tool/spatial.py`, `core/tool.py`, `core/material.py`, `core/spatial.py`, and
`bim/module/{search,spatial,type,material,group,aggregate}/operator.py`. All
tooltips document the scheme. Syntax-checked; not yet exercised in Blender.
## Things to test / verify
- Each operator × each modifier, but especially:
- SHIFT with a large selection: only objects matching the *active* object's
criteria are removed; the rest of the selection survives.
- CTRL filter: nothing new gets selected (hidden matches must not appear).
- CTRL+SHIFT still triggers the legacy behavior (one-level-deep etc.) and does
**not** also remove/filter.
- `select_ifc_class` remove/filter with a subclass selected (subtype matching).
- Aggregate operators in remove/filter mode: seed selection intact; in normal mode
behavior unchanged (seeds deselected, aggregates/parts selected).
- `select_decomposed_elements`: the trailing "make active list item the active
object" block must not re-add it in remove mode nor add it in filter mode.
- `select_similar` calculate-sum on numeric keys now requires CTRL+SHIFT; its
tooltip line renders only for numeric values.
- Redo-panel (F9) interaction: all new props are `SKIP_SAVE`, so re-running from the
panel starts clean.