diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index a68975027d..64ad5005ba 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -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 = [] diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 26d6b3a2e4..5cd03f6b3d 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -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" diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index d0faf9dad6..02612db198 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -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.") diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index c37779d7d1..101f9c3ab8 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -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"}