mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 15:08:51 +00:00
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:
@@ -291,13 +291,13 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
|||||||
objects = [context.active_object] if context.active_object else []
|
objects = [context.active_object] if context.active_object else []
|
||||||
else:
|
else:
|
||||||
objects = context.selected_objects
|
objects = context.selected_objects
|
||||||
all_parts = []
|
aggregates = {}
|
||||||
for obj in objects:
|
for obj in objects:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if element:
|
if element:
|
||||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||||
if aggregate:
|
if aggregate:
|
||||||
all_parts.append(aggregate)
|
aggregates[aggregate.id()] = aggregate
|
||||||
if not keep_current_selection:
|
if not keep_current_selection:
|
||||||
obj.select_set(False)
|
obj.select_set(False)
|
||||||
else:
|
else:
|
||||||
@@ -305,6 +305,8 @@ class BIM_OT_select_aggregate(bpy.types.Operator):
|
|||||||
if not element and not keep_current_selection:
|
if not element and not keep_current_selection:
|
||||||
obj.select_set(False)
|
obj.select_set(False)
|
||||||
|
|
||||||
|
all_parts = list(aggregates.values())
|
||||||
|
|
||||||
if self.select_parts:
|
if self.select_parts:
|
||||||
selected_parts = []
|
selected_parts = []
|
||||||
|
|
||||||
|
|||||||
@@ -75,27 +75,109 @@ class SelectByMaterial(bpy.types.Operator):
|
|||||||
return self.execute(context)
|
return self.execute(context)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
material = tool.Ifc.get().by_id(self.material)
|
# Determine the layer index hint from the explicit material prop, if any.
|
||||||
core.select_by_material(
|
# When the user clicks a specific layer in the UI, self.material is that
|
||||||
tool.Material,
|
# layer's IfcMaterial. We find its index so we can pull the same layer
|
||||||
tool.Spatial,
|
# from every other selected object's layer set.
|
||||||
material=material,
|
layer_index = None
|
||||||
should_unhide=self.should_unhide,
|
if self.material:
|
||||||
remove_from_selection=self.remove_from_selection,
|
ref_mat = tool.Ifc.get().by_id(self.material)
|
||||||
filter_selection=self.filter_selection,
|
layer_index = self._get_layer_index(ref_mat)
|
||||||
)
|
|
||||||
|
|
||||||
# copy selection query to clipboard
|
if self.remove_from_selection or self.filter_selection:
|
||||||
if material.is_a("IfcMaterialLayerSet"):
|
objects = [context.active_object] if context.active_object else []
|
||||||
material_name = material.LayerSetName
|
|
||||||
else:
|
else:
|
||||||
material_name = material.Name
|
objects = context.selected_objects
|
||||||
result = f'material="{material_name}"'
|
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
|
bpy.context.window_manager.clipboard = result
|
||||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||||
|
|
||||||
return {"FINISHED"}
|
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):
|
class EnableEditingMaterial(bpy.types.Operator):
|
||||||
bl_idname = "bim.enable_editing_material"
|
bl_idname = "bim.enable_editing_material"
|
||||||
|
|||||||
@@ -1297,7 +1297,6 @@ class SelectIfcClass(Operator):
|
|||||||
if not element or not any(element.is_a(cls) for cls in classes):
|
if not element or not any(element.is_a(cls) for cls in classes):
|
||||||
obj.select_set(False)
|
obj.select_set(False)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
result = ""
|
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
for element in tool.Ifc.get().by_type(cls):
|
for element in tool.Ifc.get().by_type(cls):
|
||||||
if (
|
if (
|
||||||
@@ -1314,13 +1313,10 @@ class SelectIfcClass(Operator):
|
|||||||
else:
|
else:
|
||||||
tool.Blender.select_object(obj)
|
tool.Blender.select_object(obj)
|
||||||
|
|
||||||
# copy selection query to clipboard
|
# copy selection query to clipboard
|
||||||
if not result:
|
result = " + ".join(classes)
|
||||||
result = f"{cls}"
|
bpy.context.window_manager.clipboard = result
|
||||||
else:
|
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||||
result += f", {cls}"
|
|
||||||
bpy.context.window_manager.clipboard = result
|
|
||||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
|
||||||
|
|
||||||
return {"FINISHED"}
|
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).",
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -1601,17 +1597,22 @@ class SelectSimilar(Operator):
|
|||||||
bpy.context.window_manager.clipboard = str(total)
|
bpy.context.window_manager.clipboard = str(total)
|
||||||
self.report({"INFO"}, f"({total}) was copied to the clipboard.")
|
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
|
key = "PredefinedType" if key == "predefined_type" else key
|
||||||
if value is True:
|
if not values:
|
||||||
value = "TRUE"
|
return
|
||||||
elif value is False:
|
|
||||||
value = "FALSE"
|
|
||||||
|
|
||||||
if isinstance(value, list) and value:
|
def format_value(value):
|
||||||
result = ", ".join(f'{key} = "{item}"' for item in value)
|
if value is True:
|
||||||
else:
|
return f'{key} = "TRUE"'
|
||||||
result = f'{key} = "{value}"'
|
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
|
bpy.context.window_manager.clipboard = result
|
||||||
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
self.report({"INFO"}, f"({result}) was copied to the clipboard.")
|
||||||
|
|||||||
@@ -303,21 +303,40 @@ class SelectSimilarContainer(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
if self.container:
|
if self.container:
|
||||||
container = tool.Ifc.get().by_id(self.container)
|
# Called from container manager panel with explicit container
|
||||||
elif element := tool.Ifc.get_entity(context.active_object):
|
ifc_container = tool.Ifc.get().by_id(self.container)
|
||||||
container = ifcopenshell.util.element.get_container(element)
|
containers = {ifc_container.id(): ifc_container} if ifc_container else {}
|
||||||
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"}
|
return {"CANCELLED"}
|
||||||
if not container:
|
|
||||||
return {"CANCELLED"}
|
for container in containers.values():
|
||||||
core.select_similar_container(
|
tool.Spatial.select_products(
|
||||||
tool.Spatial,
|
tool.Spatial.get_decomposed_elements(container, self.is_recursive),
|
||||||
container=container,
|
unhide=self.should_unhide,
|
||||||
is_recursive=self.is_recursive,
|
remove=self.remove_from_selection,
|
||||||
should_unhide=self.should_unhide,
|
filter_selection=self.filter_selection,
|
||||||
remove_from_selection=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
|
self.is_recursive = True # <-- forcibly reset
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user