diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index 84fd4250b5..fccb246143 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -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"} diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index 5318630832..e871f07e8d 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -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"} diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index f4c983928e..201aa77f46 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -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"): diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 33b2d7672b..8f22219c09 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -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: diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 86614a6a79..967059cda4 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -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"} diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 868aa0c990..183c34b320 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -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() diff --git a/src/bonsai/bonsai/core/material.py b/src/bonsai/bonsai/core/material.py index 4aea28d93d..ab88ffbcd4 100644 --- a/src/bonsai/bonsai/core/material.py +++ b/src/bonsai/bonsai/core/material.py @@ -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: diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index 3c0203b39c..3bb170edca 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -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: diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 09a4fe90f0..85216fc358 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -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 diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index f69ed31dbc..2e087996d6 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -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(