Merge PR #7940 (Concatenate_selections) to resolve build conflicts

Both branches grew from Unhide_with_alt_click and rewrote the same
select operators. This merge makes #7940 an ancestor so both PRs can
coexist in BonsaiPR builds, reconciling intents:

- select_aggregate: #7940 aggregate dedup dict + #8241
  keep_current_selection/active-object seeds
- SelectByMaterial: #7940 selection-derived materials + #8241
  remove/filter threading (active object only in those modes)
- SelectIfcClass: #7940 concatenated clipboard query + #8241 filter
  branch and deselect path
- SelectSimilarContainer: #7940 multi-container derivation and
  clipboard + #8241 remove/filter threading (active object only in
  those modes)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ryan Schultz
2026-07-05 14:33:19 -05:00
4 changed files with 151 additions and 47 deletions
@@ -291,13 +291,13 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
objects = [context.active_object] if context.active_object else []
else:
objects = context.selected_objects
all_parts = []
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)
aggregates[aggregate.id()] = aggregate
if not keep_current_selection:
obj.select_set(False)
else:
@@ -305,6 +305,8 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
if not element and not keep_current_selection:
obj.select_set(False)
all_parts = list(aggregates.values())
if self.select_parts:
selected_parts = []
@@ -75,27 +75,109 @@ class SelectByMaterial(bpy.types.Operator):
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,
remove_from_selection=self.remove_from_selection,
filter_selection=self.filter_selection,
)
# 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"}
for mat in materials.values():
core.select_by_material(
tool.Material,
tool.Spatial,
material=mat,
should_unhide=self.should_unhide,
remove_from_selection=self.remove_from_selection,
filter_selection=self.filter_selection,
)
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"
+19 -18
View File
@@ -1297,7 +1297,6 @@ class SelectIfcClass(Operator):
if not element or not any(element.is_a(cls) for cls in classes):
obj.select_set(False)
return {"FINISHED"}
result = ""
for cls in classes:
for element in tool.Ifc.get().by_type(cls):
if (
@@ -1314,13 +1313,10 @@ class SelectIfcClass(Operator):
else:
tool.Blender.select_object(obj)
# 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"}
@@ -1541,7 +1537,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"}
@@ -1601,17 +1597,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.")
@@ -303,21 +303,40 @@ class SelectSimilarContainer(bpy.types.Operator):
def execute(self, context):
if self.container:
container = tool.Ifc.get().by_id(self.container)
elif element := tool.Ifc.get_entity(context.active_object):
container = ifcopenshell.util.element.get_container(element)
# 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"}
if not container:
return {"CANCELLED"}
core.select_similar_container(
tool.Spatial,
container=container,
is_recursive=self.is_recursive,
should_unhide=self.should_unhide,
remove_from_selection=self.remove_from_selection,
filter_selection=self.filter_selection,
)
for container in containers.values():
tool.Spatial.select_products(
tool.Spatial.get_decomposed_elements(container, self.is_recursive),
unhide=self.should_unhide,
remove=self.remove_from_selection,
filter_selection=self.filter_selection,
)
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"}