Merge entities, if match on name or identification attribute only.

This commit is contained in:
Ryan Schultz
2025-10-13 11:25:36 -05:00
parent 8014980b88
commit 322a2179e5
3 changed files with 80 additions and 21 deletions
+22 -3
View File
@@ -818,7 +818,7 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_identical_objects" bl_idname = "bim.merge_identical_objects"
bl_label = "Merge Identical Objects" bl_label = "Merge Identical Objects"
bl_description = "For materials currently only IfcMaterials are supported" bl_description = "Merge identical IFC entities (that match all attributes). Hold Shift to merge by name/identification attribute only"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
@@ -826,18 +826,36 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)), items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
) )
by_name_or_identification_only: bpy.props.BoolProperty(
name="By Name/Identification Only",
description="Merge based only on Name or Identification attribute, ignoring other properties",
default=False,
)
if TYPE_CHECKING: if TYPE_CHECKING:
object_type: tool.Debug.PurgeMergeObjectType object_type: tool.Debug.PurgeMergeObjectType
def invoke(self, context, event):
# Check if shift key is pressed
if event.shift:
self.by_name_or_identification_only = True
else:
self.by_name_or_identification_only = False
return self.execute(context)
def _execute(self, context): def _execute(self, context):
object_type: str = self.object_type object_type: str = self.object_type
if object_type in ("PROFILE", "TYPE"): if object_type in ("PROFILE", "TYPE"):
self.report({"ERROR"}, f"Unsupported object type {object_type}.") self.report({"ERROR"}, f"Unsupported object type {object_type}.")
return {"CANCELLED"} return {"CANCELLED"}
merged_data = tool.Debug.merge_identical_objects(object_type) merged_data = tool.Debug.merge_identical_objects(
object_type, by_name_or_identification_only=self.by_name_or_identification_only
)
plural_object_type = f"{object_type.lower().replace('_', ' ')}s" plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
if merged_data: if merged_data:
merge_mode = " by name/identification" if self.by_name_or_identification_only else ""
for element_type, element_names in merged_data.items(): for element_type, element_names in merged_data.items():
print(f"- {element_type}:") print(f"- {element_type}:")
for name in element_names: for name in element_names:
@@ -846,7 +864,8 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
merged = sum(len(v) for v in merged_data.values()) merged = sum(len(v) for v in merged_data.values())
msg = " See system console for details." if merged else "" msg = " See system console for details." if merged else ""
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}") merge_mode = " (by name/identification)" if self.by_name_or_identification_only else ""
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged{merge_mode}.{msg}")
if merged == 0: if merged == 0:
return return
+2 -1
View File
@@ -653,4 +653,5 @@ class BIM_PT_purge(Panel):
row = layout.row(align=True) row = layout.row(align=True)
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:") row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = object_type merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
merge_op.object_type = object_type
+56 -17
View File
@@ -33,7 +33,7 @@ import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from mathutils import Vector from mathutils import Vector
from collections import defaultdict from collections import defaultdict
from typing import Literal, TYPE_CHECKING, assert_never from typing import Literal, TYPE_CHECKING, assert_never, Union
from collections.abc import Iterable from collections.abc import Iterable
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -136,10 +136,17 @@ class Debug(bonsai.core.tool.Debug):
"PERSON", "PERSON",
"PERSON_AND_ORGANIZATION", "PERSON_AND_ORGANIZATION",
], ],
by_name_or_identification_only: bool = False,
) -> dict[str, list[str]]: ) -> dict[str, list[str]]:
"""Merge identical objects. """Merge identical objects.
Note that Styles UI (or other UI) should be updated manually after using this method. Note that Styles UI (or other UI) should be updated manually after using this method.
Args:
object_type: The type of object to merge
by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
""" """
def get_hash(element: ifcopenshell.entity_instance) -> int: def get_hash(element: ifcopenshell.entity_instance) -> int:
@@ -152,6 +159,25 @@ class Debug(bonsai.core.tool.Debug):
data["TheOrganization"] = element.TheOrganization.id() data["TheOrganization"] = element.TheOrganization.id()
return hash(json.dumps(data, sort_keys=True)) return hash(json.dumps(data, sort_keys=True))
def get_name_key(element: ifcopenshell.entity_instance) -> str:
"""Get key based on name/identifier attribute for the given object type"""
if object_type == "STYLE":
return element.Name if element.Name else ""
elif object_type == "MATERIAL":
return element.Name if element.Name else ""
elif object_type == "ORGANIZATION":
return element.Name if element.Name else ""
elif object_type == "APPLICATION":
return element.ApplicationFullName if element.ApplicationFullName else ""
elif object_type == "PERSON":
return element.Identification if element.Identification else ""
elif object_type == "PERSON_AND_ORGANIZATION":
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
return f"{person_id}|{org_name}"
else:
assert_never(object_type)
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
merged_element_types: dict[str, list[str]] = {} merged_element_types: dict[str, list[str]] = {}
@@ -179,22 +205,35 @@ class Debug(bonsai.core.tool.Debug):
for element_type in element_types: for element_type in element_types:
elements = ifc_file.by_type(element_type, include_subtypes=False) elements = ifc_file.by_type(element_type, include_subtypes=False)
# Calculate hashes. # Calculate hashes or name keys.
hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list) hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
for element in elements:
# Except for styles, ignore unnamed elements as they may be not safe to merge if by_name_or_identification_only:
merge_optional_names = ("STYLE", "PERSON") # Group by name/identifier only
not_optional_name = ("APPLICATION", "ORGANIZATION") hash_to_elements = defaultdict(list)
has_no_name = ("PERSON_AND_ORGANIZATION",) for element in elements:
if ( name_key = get_name_key(element)
object_type not in merge_optional_names # Skip elements without a valid identifier
and object_type not in not_optional_name if not name_key:
and object_type not in has_no_name continue
and not element.Name hash_to_elements[name_key].append(element)
): else:
continue # Group by full hash
element_hash = get_hash(element) hash_to_elements = defaultdict(list)
hash_to_elements[element_hash].append(element) for element in elements:
# Except for styles, ignore unnamed elements as they may be not safe to merge
merge_optional_names = ("STYLE", "PERSON")
not_optional_name = ("APPLICATION", "ORGANIZATION")
has_no_name = ("PERSON_AND_ORGANIZATION",)
if (
object_type not in merge_optional_names
and object_type not in not_optional_name
and object_type not in has_no_name
and not element.Name
):
continue
element_hash = get_hash(element)
hash_to_elements[element_hash].append(element)
merged_elements_names: list[str] = [] merged_elements_names: list[str] = []
# Merge elements. # Merge elements.