mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03b3d3f627 | |||
| e6697d0956 | |||
| 69486b7d1d | |||
| 7a7bc75cba | |||
| be8595e9a3 | |||
| 6c97b6fa5e | |||
| 11be7e2b62 | |||
| 3c6d08fabf | |||
| 60ccd3ab7e | |||
| 7e3a9e4a75 | |||
| 2a91d212b3 | |||
| a14b02d32f | |||
| 20aa3ff94e | |||
| a2ec575618 | |||
| f0d8a6f4f4 | |||
| 2ad30a00d7 | |||
| e3d5e1d1fe | |||
| 15228ac8b5 | |||
| 011fb6660a | |||
| cd458780e6 | |||
| 873f9f061f | |||
| ea27b4f3ae | |||
| 1ce62c948c | |||
| f327a999d0 |
@@ -0,0 +1,184 @@
|
||||
<!-- 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.
|
||||
@@ -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,154 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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, mode=self.regex_mode)
|
||||
|
||||
|
||||
def draw_attributes(
|
||||
props: Union[bpy.types.bpy_prop_collection_idprop[Attribute], Sequence[Attribute]],
|
||||
layout: bpy.types.UILayout,
|
||||
|
||||
@@ -27,6 +27,15 @@ 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,
|
||||
selection_mode,
|
||||
)
|
||||
|
||||
|
||||
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -254,7 +263,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"
|
||||
@@ -266,64 +275,111 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
||||
one_level_deep: bpy.props.BoolProperty(
|
||||
name="One Level Deep", description="Select only immediate children, not recursively", default=False
|
||||
)
|
||||
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"})
|
||||
|
||||
regex_clipboard_key = "parent"
|
||||
regex_count_noun = "aggregates"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.select_parts:
|
||||
return "Select Aggregate and Parts.\n\nCtrl+click to select only one level deep"
|
||||
else:
|
||||
return "Select Aggregate"
|
||||
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:
|
||||
self.one_level_deep = True
|
||||
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 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")
|
||||
|
||||
def apply_regex(self, context, pattern):
|
||||
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))
|
||||
|
||||
self.select_regex_products(products)
|
||||
return len(aggregates)
|
||||
|
||||
def execute(self, context):
|
||||
all_parts = []
|
||||
for obj in context.selected_objects:
|
||||
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 []
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
aggregates = {}
|
||||
for obj in objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if element:
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
all_parts.append(aggregate)
|
||||
obj.select_set(False)
|
||||
aggregates[aggregate.id()] = aggregate
|
||||
if not keep_current_selection:
|
||||
obj.select_set(False)
|
||||
else:
|
||||
pass
|
||||
if not element:
|
||||
if not element and not keep_current_selection:
|
||||
obj.select_set(False)
|
||||
|
||||
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)
|
||||
|
||||
for element in set(selected_parts + all_parts):
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
obj.select_set(True)
|
||||
|
||||
else:
|
||||
if not self.select_parts and not keep_current_selection:
|
||||
for aggregate_element in all_parts:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate_element)
|
||||
if aggregate_obj:
|
||||
aggregate_obj.select_set(True)
|
||||
if aggregate_obj := tool.Ifc.get_object(aggregate_element):
|
||||
bpy.context.view_layer.objects.active = aggregate_obj
|
||||
|
||||
# copy selection query to clipboard
|
||||
@@ -407,17 +463,36 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
|
||||
bl_label = "Select linked aggregates"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
select_parts: bpy.props.BoolProperty(default=False)
|
||||
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"})
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.select_parts:
|
||||
return "Select all aggregates, subaggregates and all their parts"
|
||||
else:
|
||||
return "Select all aggregates"
|
||||
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):
|
||||
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):
|
||||
for obj in context.selected_objects:
|
||||
obj.select_set(False)
|
||||
keep_current_selection = self.remove_from_selection or self.filter_selection
|
||||
if keep_current_selection:
|
||||
objects = [context.active_object] if context.active_object else []
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
products = set()
|
||||
for obj in objects:
|
||||
if not keep_current_selection:
|
||||
obj.select_set(False)
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if not aggregate:
|
||||
@@ -436,21 +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:
|
||||
obj.select_set(True)
|
||||
else:
|
||||
for element in parts:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
obj.select_set(True)
|
||||
tool.Spatial.select_products(
|
||||
products,
|
||||
unhide=self.should_unhide,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -24,6 +24,15 @@ 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,
|
||||
selection_mode,
|
||||
)
|
||||
|
||||
|
||||
class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -191,23 +200,60 @@ 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\nALT + CLICK to exclude children"
|
||||
"Select objects assigned to the selected group and all nested groups"
|
||||
+ 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"})
|
||||
|
||||
regex_clipboard_key = "group"
|
||||
regex_count_noun = "groups"
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.is_recursive = not event.alt
|
||||
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 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"):
|
||||
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))
|
||||
self.select_regex_products(products)
|
||||
return matched_groups
|
||||
|
||||
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)
|
||||
ifcopenshell.util.element.get_grouped_by(tool.Ifc.get().by_id(self.group), is_recursive=self.is_recursive),
|
||||
unhide=self.should_unhide,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -30,6 +30,15 @@ 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,
|
||||
selection_mode,
|
||||
)
|
||||
from bonsai.bim.module.model import slab, wall
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -58,28 +67,164 @@ 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"
|
||||
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"})
|
||||
|
||||
regex_clipboard_key = "material"
|
||||
|
||||
def invoke(self, context, event):
|
||||
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 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)
|
||||
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 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):
|
||||
material = tool.Ifc.get().by_id(self.material)
|
||||
core.select_by_material(tool.Material, tool.Spatial, material=material)
|
||||
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
|
||||
# from every other selected object's layer set.
|
||||
layer_index = None
|
||||
if self.material:
|
||||
ref_mat = tool.Ifc.get().by_id(self.material)
|
||||
layer_index = self._get_layer_index(ref_mat)
|
||||
|
||||
# copy selection query to clipboard
|
||||
if material.is_a("IfcMaterialLayerSet"):
|
||||
material_name = material.LayerSetName
|
||||
if self.remove_from_selection or self.filter_selection:
|
||||
objects = [context.active_object] if context.active_object else []
|
||||
else:
|
||||
material_name = material.Name
|
||||
result = f'material="{material_name}"'
|
||||
objects = context.selected_objects
|
||||
materials = {}
|
||||
for obj in objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
mat = ifcopenshell.util.element.get_material(element)
|
||||
if not mat:
|
||||
continue
|
||||
|
||||
resolved = self._resolve_material(mat, layer_index)
|
||||
if resolved:
|
||||
materials[resolved.id()] = resolved
|
||||
|
||||
# Fall back to the explicit material prop if selection yields nothing
|
||||
if not materials and self.material:
|
||||
materials = {self.material: tool.Ifc.get().by_id(self.material)}
|
||||
|
||||
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, mode=mode
|
||||
)
|
||||
|
||||
result = " + ".join(f'material = "{self._get_name(m)}"' for m in materials.values())
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def _get_layer_index(self, material):
|
||||
"""Return the 0-based layer index if material is an IfcMaterial inside a layer set."""
|
||||
if not material.is_a("IfcMaterial"):
|
||||
return None
|
||||
ifc = tool.Ifc.get()
|
||||
for layer in ifc.get_inverse(material):
|
||||
if not layer.is_a("IfcMaterialLayer"):
|
||||
continue
|
||||
for layer_set in ifc.get_inverse(layer):
|
||||
if not layer_set.is_a("IfcMaterialLayerSet"):
|
||||
continue
|
||||
layers = list(layer_set.MaterialLayers)
|
||||
if layer in layers:
|
||||
return layers.index(layer)
|
||||
return None
|
||||
|
||||
def _resolve_material(self, mat, layer_index):
|
||||
"""Resolve an assigned material to the specific entity to select/name by.
|
||||
|
||||
When layer_index is set, drills into the layer set and returns the
|
||||
IfcMaterial at that index (or None if the set has fewer layers).
|
||||
Otherwise returns the layer set / profile set / constituent set itself.
|
||||
"""
|
||||
if mat.is_a("IfcMaterialLayerSetUsage"):
|
||||
mat = mat.ForLayerSet
|
||||
elif mat.is_a("IfcMaterialProfileSetUsage"):
|
||||
mat = mat.ForProfileSet
|
||||
|
||||
if layer_index is not None and mat.is_a("IfcMaterialLayerSet"):
|
||||
layers = list(mat.MaterialLayers)
|
||||
if layer_index < len(layers):
|
||||
return layers[layer_index].Material
|
||||
return None
|
||||
|
||||
return mat
|
||||
|
||||
def _get_name(self, material):
|
||||
if material.is_a("IfcMaterialLayerSet"):
|
||||
if material.LayerSetName:
|
||||
return material.LayerSetName
|
||||
names = [l.Material.Name for l in (material.MaterialLayers or []) if l.Material and l.Material.Name]
|
||||
return ", ".join(names) if names else material.is_a()
|
||||
if material.is_a("IfcMaterialProfileSet"):
|
||||
if material.Name:
|
||||
return material.Name
|
||||
names = [p.Material.Name for p in (material.MaterialProfiles or []) if p.Material and p.Material.Name]
|
||||
return ", ".join(names) if names else material.is_a()
|
||||
if material.is_a("IfcMaterialConstituentSet"):
|
||||
if material.Name:
|
||||
return material.Name
|
||||
names = [c.Material.Name for c in (material.MaterialConstituents or []) if c.Material and c.Material.Name]
|
||||
return ", ".join(names) if names else material.is_a()
|
||||
return getattr(material, "Name", None) or material.is_a()
|
||||
|
||||
|
||||
class EnableEditingMaterial(bpy.types.Operator):
|
||||
bl_idname = "bim.enable_editing_material"
|
||||
|
||||
@@ -38,6 +38,15 @@ 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,
|
||||
selection_mode,
|
||||
)
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from bonsai.bim.prop import StrProperty
|
||||
|
||||
@@ -1264,26 +1273,40 @@ class SelectGlobalId(Operator):
|
||||
|
||||
|
||||
class SelectIfcClass(Operator):
|
||||
"""Click to select all objects that match with the given IFC class\nSHIFT + Click to also match Predefined Type"""
|
||||
|
||||
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)
|
||||
remove_from_selection: BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
filter_selection: BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.should_filter_predefined_type = event.shift
|
||||
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):
|
||||
objects = context.selected_objects
|
||||
if self.remove_from_selection or self.filter_selection:
|
||||
objects = [context.active_object] if context.active_object else []
|
||||
else:
|
||||
objects = context.selected_objects
|
||||
classes = set()
|
||||
predefined_types = set()
|
||||
for obj in objects:
|
||||
if element := tool.Ifc.get_entity(obj):
|
||||
classes.add(element.is_a())
|
||||
predefined_types.add(ifcopenshell.util.element.get_predefined_type(element))
|
||||
result = ""
|
||||
|
||||
elements = []
|
||||
for cls in classes:
|
||||
for element in tool.Ifc.get().by_type(cls):
|
||||
if (
|
||||
@@ -1291,16 +1314,17 @@ class SelectIfcClass(Operator):
|
||||
and ifcopenshell.util.element.get_predefined_type(element) not in predefined_types
|
||||
):
|
||||
continue
|
||||
if obj := tool.Ifc.get_object(element):
|
||||
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
|
||||
if not result:
|
||||
result = f"{cls}"
|
||||
else:
|
||||
result += f", {cls}"
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
# copy selection query to clipboard
|
||||
result = " + ".join(classes)
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1440,7 +1464,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"}
|
||||
@@ -1451,10 +1475,18 @@ class SelectSimilar(Operator):
|
||||
)
|
||||
calculated_sum: bpy.props.FloatProperty(name="Calculated Sum", default=0.0)
|
||||
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"})
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
base = "Select objects with a similar value\n\n" "SHIFT+CLICK remove from selection set."
|
||||
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
|
||||
@@ -1467,7 +1499,7 @@ class SelectSimilar(Operator):
|
||||
|
||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||
if isinstance(value, (int, float)):
|
||||
return base + ("\nCTRL+CLICK display the sum of all selected objects")
|
||||
return base + ("\nCTRL+SHIFT+Click to display the sum of all selected objects")
|
||||
else:
|
||||
return base
|
||||
|
||||
@@ -1479,10 +1511,34 @@ class SelectSimilar(Operator):
|
||||
return False
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.calculate_sum = event.ctrl and event.type == "LEFTMOUSE"
|
||||
self.remove_from_selection = event.shift and event.type == "LEFTMOUSE"
|
||||
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 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
|
||||
key = "predefined_type" if self.key == "PredefinedType" else self.key
|
||||
@@ -1490,6 +1546,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)
|
||||
|
||||
if self.calculate_sum:
|
||||
self._calculate_sum(context, key)
|
||||
else:
|
||||
@@ -1499,7 +1558,12 @@ class SelectSimilar(Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
matched_count = self._select_objects(context, key, reference_values, tolerance)
|
||||
verb = "Deselected" if self.remove_from_selection else "Selected"
|
||||
if self.filter_selection:
|
||||
verb = "Filtered selection to"
|
||||
elif self.remove_from_selection:
|
||||
verb = "Deselected"
|
||||
else:
|
||||
verb = "Selected"
|
||||
|
||||
if all(isinstance(v, (int, float)) for v in reference_values):
|
||||
self.report(
|
||||
@@ -1512,7 +1576,7 @@ class SelectSimilar(Operator):
|
||||
f"{verb} all objects that share the same ({self.key}) value(s) from {len(reference_values)} reference object(s).",
|
||||
)
|
||||
|
||||
self._generate_clipboard_query(reference_values[0] if reference_values else None, key)
|
||||
self._generate_clipboard_query(reference_values, key)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1525,7 +1589,7 @@ class SelectSimilar(Operator):
|
||||
def _get_reference_values(self, context, key):
|
||||
objects = (
|
||||
[context.active_object]
|
||||
if self.remove_from_selection
|
||||
if self.remove_from_selection or self.filter_selection
|
||||
else (context.selected_objects or [context.active_object])
|
||||
)
|
||||
values = [self._get_value(obj, key) for obj in objects]
|
||||
@@ -1538,11 +1602,26 @@ class SelectSimilar(Operator):
|
||||
|
||||
def _select_objects(self, context, key, reference_values, tolerance):
|
||||
count = 0
|
||||
for obj in context.visible_objects:
|
||||
if self.filter_selection:
|
||||
# Keep only the already selected objects that match, select nothing new.
|
||||
for obj in context.selected_objects:
|
||||
obj_value = self._get_value(obj, key)
|
||||
if obj_value is not None and any(
|
||||
self._compare_values(obj_value, ref_value, tolerance) for ref_value in reference_values
|
||||
):
|
||||
count += 1
|
||||
else:
|
||||
obj.select_set(False)
|
||||
return count
|
||||
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 None:
|
||||
continue
|
||||
if any(self._compare_values(obj_value, ref_value, tolerance) for ref_value in reference_values):
|
||||
if self.should_unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
obj.select_set(not self.remove_from_selection)
|
||||
count += 1
|
||||
return count
|
||||
@@ -1557,17 +1636,22 @@ class SelectSimilar(Operator):
|
||||
bpy.context.window_manager.clipboard = str(total)
|
||||
self.report({"INFO"}, f"({total}) was copied to the clipboard.")
|
||||
|
||||
def _generate_clipboard_query(self, value, key):
|
||||
def _generate_clipboard_query(self, values, key):
|
||||
key = "PredefinedType" if key == "predefined_type" else key
|
||||
if value is True:
|
||||
value = "TRUE"
|
||||
elif value is False:
|
||||
value = "FALSE"
|
||||
if not values:
|
||||
return
|
||||
|
||||
if isinstance(value, list) and value:
|
||||
result = ", ".join(f'{key} = "{item}"' for item in value)
|
||||
else:
|
||||
result = f'{key} = "{value}"'
|
||||
def format_value(value):
|
||||
if value is True:
|
||||
return f'{key} = "TRUE"'
|
||||
elif value is False:
|
||||
return f'{key} = "FALSE"'
|
||||
elif isinstance(value, list) and value:
|
||||
return ", ".join(f'{key} = "{item}"' for item in value)
|
||||
else:
|
||||
return f'{key} = "{value}"'
|
||||
|
||||
result = " + ".join(format_value(v) for v in values)
|
||||
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
|
||||
@@ -24,6 +24,13 @@ 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,
|
||||
selection_mode,
|
||||
)
|
||||
|
||||
|
||||
class ReferenceStructure(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -284,23 +291,65 @@ class SelectContainer(bpy.types.Operator):
|
||||
class SelectSimilarContainer(bpy.types.Operator):
|
||||
bl_idname = "bim.select_similar_container"
|
||||
bl_label = "Select Similar Container"
|
||||
bl_description = "Recurvisevly selects all objects in the container.\n\nCtrl+click to select only one level deep"
|
||||
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)
|
||||
is_recursive: bpy.props.BoolProperty(default=True)
|
||||
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"})
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.type == "LEFTMOUSE" and event.ctrl:
|
||||
self.is_recursive = False
|
||||
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):
|
||||
core.select_similar_container(
|
||||
tool.Ifc,
|
||||
tool.Spatial,
|
||||
obj=context.active_object,
|
||||
is_recursive=self.is_recursive,
|
||||
)
|
||||
if self.container:
|
||||
# Called from container manager panel with explicit container
|
||||
ifc_container = tool.Ifc.get().by_id(self.container)
|
||||
containers = {ifc_container.id(): ifc_container} if ifc_container else {}
|
||||
else:
|
||||
# Called from 3D viewport — derive containers from the selected objects
|
||||
# (active object only in remove/filter mode, so a single criteria source)
|
||||
if self.remove_from_selection or self.filter_selection:
|
||||
objects = [context.active_object] if context.active_object else []
|
||||
else:
|
||||
objects = context.selected_objects or [context.active_object]
|
||||
containers = {}
|
||||
for obj in objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
container = tool.Spatial.get_container(element)
|
||||
if container:
|
||||
containers[container.id()] = container
|
||||
|
||||
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,
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
result = " + ".join(f'location = "{c.Name}"' for c in containers.values())
|
||||
bpy.context.window_manager.clipboard = result
|
||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||
|
||||
self.is_recursive = True # <-- forcibly reset
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -425,24 +474,34 @@ class SelectDecomposedElements(bpy.types.Operator):
|
||||
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
|
||||
container: bpy.props.IntProperty()
|
||||
is_recursive: bpy.props.BoolProperty(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"})
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, operator):
|
||||
return (
|
||||
"Select the active item"
|
||||
+ "\nALT+CLICK to select all listed elements.\nCTRL + CLICK to select only one level deep"
|
||||
+ 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.alt:
|
||||
self.should_filter = False
|
||||
if event.ctrl:
|
||||
self.is_recursive = False
|
||||
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):
|
||||
tool.Spatial.select_products(tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive))
|
||||
tool.Spatial.select_products(
|
||||
tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive),
|
||||
unhide=self.should_unhide,
|
||||
mode=selection_mode(self.remove_from_selection, self.filter_selection),
|
||||
)
|
||||
|
||||
# Make selected active element in list, the active object
|
||||
props = tool.Spatial.get_spatial_props()
|
||||
@@ -453,7 +512,8 @@ class SelectDecomposedElements(bpy.types.Operator):
|
||||
obj = tool.Ifc.get_object(ifc_entity)
|
||||
if obj:
|
||||
context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
if not self.filter_selection:
|
||||
obj.select_set(not self.remove_from_selection)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -525,6 +585,11 @@ class SetContainerVisibility(bpy.types.Operator):
|
||||
if obj := tool.Ifc.get_object(container):
|
||||
if collection := tool.Blender.get_object_bim_props(obj).collection:
|
||||
collection.hide_viewport = should_hide
|
||||
if not should_hide:
|
||||
for element in tool.Spatial.get_decomposed_elements(container, is_recursive=False):
|
||||
if element_obj := tool.Ifc.get_object(element):
|
||||
element_obj.hide_viewport = False
|
||||
element_obj.hide_set(False)
|
||||
if self.should_include_children:
|
||||
queue.extend(ifcopenshell.util.element.get_parts(container))
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -139,7 +139,7 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
op = col.operator("bim.set_default_container", icon="OUTLINER_COLLECTION", text="Set Default")
|
||||
op.container = ifc_definition_id
|
||||
|
||||
if tool.Blender.get_addon_preferences().container_hide_show_isolate:
|
||||
if tool.Blender.get_addon_preferences().show_container_tools:
|
||||
op = row.operator("bim.set_container_visibility", icon="FULLSCREEN_EXIT", text="")
|
||||
op.mode = "ISOLATE"
|
||||
op.container = ifc_definition_id
|
||||
@@ -152,7 +152,10 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
|
||||
# The only operator that's enabled for IfcProject.
|
||||
col = row.column(align=True)
|
||||
col.operator("bim.select_container", icon="OBJECT_DATA", text="").container = ifc_definition_id
|
||||
row_ = col.row(align=True)
|
||||
row_.operator("bim.select_container", icon="OBJECT_DATA", text="").container = ifc_definition_id
|
||||
if tool.Blender.get_addon_preferences().show_container_tools:
|
||||
row_.operator("bim.select_similar_container", icon="RESTRICT_SELECT_OFF", text="").container = ifc_definition_id
|
||||
|
||||
col = row.column(align=True)
|
||||
op = col.operator("bim.delete_container", icon="X", text="")
|
||||
|
||||
@@ -29,6 +29,15 @@ 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,
|
||||
selection_mode,
|
||||
)
|
||||
|
||||
|
||||
class AssignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -220,15 +229,60 @@ class SelectType(bpy.types.Operator):
|
||||
return collection_in_view_layer
|
||||
|
||||
|
||||
class SelectSimilarType(bpy.types.Operator):
|
||||
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"})
|
||||
|
||||
regex_clipboard_key = "type"
|
||||
|
||||
def invoke(self, context, event):
|
||||
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 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)
|
||||
if not element:
|
||||
return None
|
||||
relating_type = ifcopenshell.util.element.get_type(element)
|
||||
if not relating_type:
|
||||
return None
|
||||
return relating_type.Name
|
||||
|
||||
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()
|
||||
objects = bpy.context.selected_objects
|
||||
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:
|
||||
objects = bpy.context.selected_objects
|
||||
|
||||
# store relating types to avoid selecting same elements multiple times
|
||||
relating_types = set()
|
||||
@@ -240,22 +294,27 @@ class SelectSimilarType(bpy.types.Operator):
|
||||
continue
|
||||
relating_types.add(relating_type)
|
||||
|
||||
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 obj in context.visible_objects:
|
||||
obj.select_set(True)
|
||||
|
||||
# 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.")
|
||||
|
||||
|
||||
@@ -151,7 +151,8 @@ class BIM_PT_type_attributes(Panel):
|
||||
row = layout.row(align=True)
|
||||
row.label(text=attribute["name"])
|
||||
value = get_display_value(attribute["value"])
|
||||
row.label(text=value)
|
||||
op = row.operator("bim.select_similar", text=value, icon="NONE", emboss=False)
|
||||
op.key = f"type.{attribute['name']}"
|
||||
|
||||
|
||||
def add_object_button(self, context):
|
||||
|
||||
@@ -714,9 +714,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
description="Default parameters for BIM elements",
|
||||
)
|
||||
|
||||
container_hide_show_isolate: BoolProperty(
|
||||
name="Container hide/show/isolate",
|
||||
description="Enable container hide/show/isolate feature in the UI",
|
||||
show_container_tools: BoolProperty(
|
||||
name="Show Container Tools (Select, Hide, Show, Isolate)",
|
||||
description="Enable container select, hide/show/isolate tools in the UI",
|
||||
default=False,
|
||||
)
|
||||
|
||||
@@ -786,7 +786,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
pset_dir: str
|
||||
doc: DocPreferences
|
||||
default_parameters: DefaultParameters
|
||||
container_hide_show_isolate: bool
|
||||
show_container_tools: bool
|
||||
chain_filter_with_set_operations: bool
|
||||
save_metadata_blend_file: bool
|
||||
metadata_blend_file_suffix: str
|
||||
@@ -984,7 +984,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
layout.prop(self, "bsdd_baseurl")
|
||||
|
||||
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "container_hide_show_isolate")
|
||||
layout.prop(self, "show_container_tools")
|
||||
row = layout.row(align=True)
|
||||
row.prop(self, "chain_filter_with_set_operations")
|
||||
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
|
||||
|
||||
@@ -82,9 +82,13 @@ def disable_editing_materials(material: type[tool.Material]) -> None:
|
||||
|
||||
|
||||
def select_by_material(
|
||||
material_tool: type[tool.Material], spatial: type[tool.Spatial], material: ifcopenshell.entity_instance
|
||||
material_tool: type[tool.Material],
|
||||
spatial: type[tool.Spatial],
|
||||
material: ifcopenshell.entity_instance,
|
||||
should_unhide: bool = False,
|
||||
mode: str = "ADD",
|
||||
) -> None:
|
||||
spatial.select_products(material_tool.get_elements_by_material(material))
|
||||
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:
|
||||
|
||||
@@ -119,17 +119,6 @@ def select_container(
|
||||
spatial.set_active_object(ifc.get_object(container), selection_mode=selection_mode)
|
||||
|
||||
|
||||
def select_similar_container(
|
||||
ifc: type[tool.Ifc],
|
||||
spatial: type[tool.Spatial],
|
||||
obj: bpy.types.Object,
|
||||
is_recursive: bool = True,
|
||||
) -> None:
|
||||
element = ifc.get_entity(obj)
|
||||
if element:
|
||||
spatial.select_products(spatial.get_decomposed_elements(spatial.get_container(element), is_recursive))
|
||||
|
||||
|
||||
def select_product(spatial: type[tool.Spatial], product: ifcopenshell.entity_instance) -> None:
|
||||
spatial.select_products([product])
|
||||
|
||||
|
||||
@@ -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): 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
|
||||
|
||||
@@ -192,16 +192,33 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
target_obj.matrix_world = relative_to_obj.matrix_world @ matrix
|
||||
|
||||
@classmethod
|
||||
def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None:
|
||||
def select_products(
|
||||
cls,
|
||||
products: Iterable[ifcopenshell.entity_instance],
|
||||
unhide: 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 mode == "FILTER":
|
||||
# Keep only the already selected objects that match, select nothing new.
|
||||
matched_objs = set()
|
||||
for product in products:
|
||||
obj = tool.Ifc.get_object(product)
|
||||
if obj and view_layer.objects.get(obj.name):
|
||||
matched_objs.add(obj)
|
||||
for obj in bpy.context.selected_objects:
|
||||
if obj not in matched_objs:
|
||||
obj.select_set(False)
|
||||
return
|
||||
for product in products:
|
||||
obj = tool.Ifc.get_object(product)
|
||||
if obj and view_layer.objects.get(obj.name):
|
||||
if unhide:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_set(False)
|
||||
obj.select_set(True)
|
||||
obj.select_set(mode != "REMOVE")
|
||||
|
||||
@classmethod
|
||||
def filter_products(
|
||||
|
||||
Reference in New Issue
Block a user