Add SHIFT+Click remove-from-selection to select operators

Follows the bim.select_similar precedent: SHIFT+Click deselects the
matched objects instead of selecting them, with criteria taken from
the active object only. Applied to select_ifc_class,
select_similar_type, select_by_material, select_similar_container,
select_decomposed_elements, select_group_elements, select_aggregate
and select_linked_aggregates, threaded through
Spatial.select_products(remove=...).

SHIFT bindings this overwrites (to be reworked later):
- select_ifc_class: match Predefined Type
- select_decomposed_elements: select all listed elements

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ryan Schultz
2026-07-02 20:47:18 -05:00
parent 60ccd3ab7e
commit 3c6d08fabf
10 changed files with 86 additions and 34 deletions
@@ -267,32 +267,39 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
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"})
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select Aggregate and Parts.\n\nCtrl+click to select only one level deep\nALT+Click to also unhide hidden objects (viewport and local hide)"
return "Select Aggregate and Parts.\n\nCtrl+click to select only one level deep\nSHIFT+Click to remove from selection set\nALT+Click to also unhide hidden objects (viewport and local hide)"
else:
return "Select Aggregate\n\nALT+Click to also unhide hidden objects (viewport and local hide)"
return "Select Aggregate\n\nSHIFT+Click to remove from selection set\nALT+Click to also unhide hidden objects (viewport and local hide)"
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.ctrl:
self.one_level_deep = True
self.should_unhide = event.alt
self.remove_from_selection = event.shift
return self.execute(context)
def execute(self, context):
if self.remove_from_selection:
objects = [context.active_object] if context.active_object else []
else:
objects = context.selected_objects
all_parts = []
for obj in context.selected_objects:
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)
if not self.remove_from_selection:
obj.select_set(False)
else:
pass
if not element:
if not element and not self.remove_from_selection:
obj.select_set(False)
if self.select_parts:
@@ -322,7 +329,7 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
obj.select_set(not self.remove_from_selection)
else:
for aggregate_element in all_parts:
@@ -331,8 +338,9 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
if self.should_unhide:
aggregate_obj.hide_viewport = False
aggregate_obj.hide_set(False)
aggregate_obj.select_set(True)
bpy.context.view_layer.objects.active = aggregate_obj
aggregate_obj.select_set(not self.remove_from_selection)
if not self.remove_from_selection:
bpy.context.view_layer.objects.active = aggregate_obj
# copy selection query to clipboard
result = ""
@@ -416,21 +424,28 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
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"})
@classmethod
def description(cls, context, properties):
if properties.select_parts:
return "Select all aggregates, subaggregates and all their parts\n\nALT+Click to also unhide hidden objects (viewport and local hide)"
return "Select all aggregates, subaggregates and all their parts\n\nSHIFT+Click to remove from selection set\nALT+Click to also unhide hidden objects (viewport and local hide)"
else:
return "Select all aggregates\n\nALT+Click to also unhide hidden objects (viewport and local hide)"
return "Select all aggregates\n\nSHIFT+Click to remove from selection set\nALT+Click to also unhide hidden objects (viewport and local hide)"
def invoke(self, context, event):
self.should_unhide = event.alt
self.remove_from_selection = event.shift
return self.execute(context)
def execute(self, context):
for obj in context.selected_objects:
obj.select_set(False)
if self.remove_from_selection:
objects = [context.active_object] if context.active_object else []
else:
objects = context.selected_objects
for obj in objects:
if not self.remove_from_selection:
obj.select_set(False)
element = tool.Ifc.get_entity(obj)
aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate:
@@ -462,7 +477,7 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
obj.select_set(not self.remove_from_selection)
else:
for element in parts:
obj = tool.Ifc.get_object(element)
@@ -470,7 +485,7 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
obj.select_set(not self.remove_from_selection)
return {"FINISHED"}
@@ -197,22 +197,26 @@ class SelectGroupElements(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Select objects assigned to the selected group and all nested groups"
"\nSHIFT + CLICK to remove from selection set"
"\nCTRL + CLICK to exclude children"
"\nALT + CLICK to also unhide hidden objects (viewport and local hide)"
)
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"})
def invoke(self, context, event):
self.is_recursive = not event.ctrl
self.should_unhide = event.alt
self.remove_from_selection = event.shift
return self.execute(context)
def execute(self, context):
tool.Spatial.select_products(
ifcopenshell.util.element.get_grouped_by(tool.Ifc.get().by_id(self.group), is_recursive=self.is_recursive),
unhide=self.should_unhide,
remove=self.remove_from_selection,
)
return {"FINISHED"}
@@ -61,18 +61,26 @@ class DisableEditingMaterials(bpy.types.Operator):
class SelectByMaterial(bpy.types.Operator):
bl_idname = "bim.select_by_material"
bl_label = "Select By Material"
bl_description = "Select objects using the provided material\n\nALT+Click to also unhide hidden objects (viewport and local hide)"
bl_description = "Select objects using the provided material\n\nSHIFT+Click to remove from selection set\nALT+Click to also unhide hidden objects (viewport and local hide)"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
should_unhide: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
remove_from_selection: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.should_unhide = event.alt
self.remove_from_selection = event.shift
return self.execute(context)
def execute(self, context):
material = tool.Ifc.get().by_id(self.material)
core.select_by_material(tool.Material, tool.Spatial, material=material, should_unhide=self.should_unhide)
core.select_by_material(
tool.Material,
tool.Spatial,
material=material,
should_unhide=self.should_unhide,
remove_from_selection=self.remove_from_selection,
)
# copy selection query to clipboard
if material.is_a("IfcMaterialLayerSet"):
@@ -1264,21 +1264,25 @@ 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\nALT + Click to also unhide hidden objects (viewport and local hide)"""
"""Click to select all objects that match with the given IFC class\nSHIFT + Click to remove from selection set\nALT + Click to also unhide hidden objects (viewport and local hide)"""
bl_idname = "bim.select_ifc_class"
bl_label = "Select IFC Class"
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"})
def invoke(self, context, event):
self.should_filter_predefined_type = event.shift
self.remove_from_selection = event.shift
self.should_unhide = event.alt
return self.execute(context)
def execute(self, context):
objects = context.selected_objects
if self.remove_from_selection:
objects = [context.active_object] if context.active_object else []
else:
objects = context.selected_objects
classes = set()
predefined_types = set()
for obj in objects:
@@ -1297,7 +1301,10 @@ class SelectIfcClass(Operator):
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
tool.Blender.select_object(obj)
if self.remove_from_selection:
tool.Blender.deselect_object(obj)
else:
tool.Blender.select_object(obj)
# copy selection query to clipboard
if not result:
@@ -284,17 +284,19 @@ class SelectContainer(bpy.types.Operator):
class SelectSimilarContainer(bpy.types.Operator):
bl_idname = "bim.select_similar_container"
bl_label = "Select Similar Container"
bl_description = "Recursively selects all objects in the container.\n\nCtrl+click to select only one level deep\nAlt+click to also unhide hidden objects (viewport and local hide)"
bl_description = "Recursively selects all objects in the container.\n\nCtrl+click to select only one level deep\nShift+click to remove from selection set\nAlt+click to also unhide hidden objects (viewport and local hide)"
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"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.ctrl:
self.is_recursive = False
self.should_unhide = event.alt
self.remove_from_selection = event.shift
return self.execute(context)
def execute(self, context):
@@ -311,6 +313,7 @@ class SelectSimilarContainer(bpy.types.Operator):
container=container,
is_recursive=self.is_recursive,
should_unhide=self.should_unhide,
remove_from_selection=self.remove_from_selection,
)
self.is_recursive = True # <-- forcibly reset
return {"FINISHED"}
@@ -437,27 +440,29 @@ class SelectDecomposedElements(bpy.types.Operator):
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"})
@classmethod
def description(cls, context, operator):
return (
"Select the active item"
+ "\nSHIFT+CLICK to select all listed elements.\nCTRL+CLICK to select only one level deep"
+ "\nSHIFT+CLICK to remove from selection set.\nCTRL+CLICK to select only one level deep"
+ "\nALT+CLICK to also unhide hidden objects (viewport and local hide)"
)
def invoke(self, context, event):
if event.type == "LEFTMOUSE":
if event.shift:
self.should_filter = False
if event.ctrl:
self.is_recursive = False
self.should_unhide = event.alt
self.remove_from_selection = event.shift
return self.execute(context)
def execute(self, context):
tool.Spatial.select_products(
tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive), unhide=self.should_unhide
tool.Spatial.get_filtered_elements(self.should_filter, self.is_recursive),
unhide=self.should_unhide,
remove=self.remove_from_selection,
)
# Make selected active element in list, the active object
@@ -469,7 +474,7 @@ class SelectDecomposedElements(bpy.types.Operator):
obj = tool.Ifc.get_object(ifc_entity)
if obj:
context.view_layer.objects.active = obj
obj.select_set(True)
obj.select_set(not self.remove_from_selection)
return {"FINISHED"}
@@ -221,21 +221,26 @@ class SelectType(bpy.types.Operator):
class SelectSimilarType(bpy.types.Operator):
"""Select Similar Type\nALT+Click to also unhide hidden objects (viewport and local hide)"""
"""Select Similar Type\nSHIFT+Click to remove from selection set\nALT+Click to also unhide hidden objects (viewport and local hide)"""
bl_idname = "bim.select_similar_type"
bl_label = "Select Similar Type"
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"})
def invoke(self, context, event):
self.should_unhide = event.alt
self.remove_from_selection = event.shift
return self.execute(context)
def execute(self, context):
self.file = tool.Ifc.get()
objects = bpy.context.selected_objects
if self.remove_from_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()
@@ -257,7 +262,7 @@ class SelectSimilarType(bpy.types.Operator):
if self.should_unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
obj.select_set(not self.remove_from_selection)
# copy selection query to clipboard
related_objects_class = related_objects[0].is_a()
+4 -1
View File
@@ -86,8 +86,11 @@ def select_by_material(
spatial: type[tool.Spatial],
material: ifcopenshell.entity_instance,
should_unhide: bool = False,
remove_from_selection: bool = False,
) -> None:
spatial.select_products(material_tool.get_elements_by_material(material), unhide=should_unhide)
spatial.select_products(
material_tool.get_elements_by_material(material), unhide=should_unhide, remove=remove_from_selection
)
def enable_editing_material(material_tool: type[tool.Material], material: ifcopenshell.entity_instance) -> None:
+4 -1
View File
@@ -124,8 +124,11 @@ def select_similar_container(
container: ifcopenshell.entity_instance,
is_recursive: bool = True,
should_unhide: bool = False,
remove_from_selection: bool = False,
) -> None:
spatial.select_products(spatial.get_decomposed_elements(container, is_recursive), unhide=should_unhide)
spatial.select_products(
spatial.get_decomposed_elements(container, is_recursive), unhide=should_unhide, remove=remove_from_selection
)
def select_product(spatial: type[tool.Spatial], product: ifcopenshell.entity_instance) -> None:
+1 -1
View File
@@ -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, remove=False): 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
+4 -2
View File
@@ -192,7 +192,9 @@ 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, remove: bool = False
) -> None:
assert (view_layer := bpy.context.view_layer)
# Update view layer, otherwise `objects` might be missing just created objects.
view_layer.update()
@@ -202,7 +204,7 @@ class Spatial(bonsai.core.tool.Spatial):
if unhide:
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
obj.select_set(not remove)
@classmethod
def filter_products(