extend https://github.com/IfcOpenShell/IfcOpenShell/commit/322a2179e51a9ec4802fe9e35039c7e3e29f9678: Merges names with number suffix, as well (ex: foo, foo.001, foo.002)

This commit is contained in:
Ryan Schultz
2025-10-14 11:29:44 -05:00
parent 322a2179e5
commit b0ae0a2c82
2 changed files with 55 additions and 2 deletions
@@ -818,7 +818,12 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_identical_objects"
bl_label = "Merge Identical Objects"
bl_description = "Merge identical IFC entities (that match all attributes). Hold Shift to merge by name/identification attribute only"
bl_description = (
"Merge identical IFC objects (that match all attributes).\n"
"\n"
"SHIFT + CLICK to merge by name/identification attribute only.\n"
"Merges names with number suffix, as well (ex: foo, foo.001, foo.002)\n"
)
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
+49 -1
View File
@@ -18,6 +18,7 @@
from __future__ import annotations
import os
import re
import json
import bmesh
import bpy
@@ -145,10 +146,21 @@ class Debug(bonsai.core.tool.Debug):
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).
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
"""
def normalize_name(name: str) -> str:
"""Remove .XXX suffix pattern from names (e.g., 'foo.001' -> 'foo')"""
if not name:
return ""
# Match pattern: name ending with .digits
match = re.match(r"^(.+)\.\d+$", name)
if match:
return match.group(1)
return name
def get_hash(element: ifcopenshell.entity_instance) -> int:
data = element.get_info_2(include_identifier=False, recursive=True)
if object_type == "APPLICATION":
@@ -161,6 +173,30 @@ class Debug(bonsai.core.tool.Debug):
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":
name = element.Name if element.Name else ""
return normalize_name(name)
elif object_type == "MATERIAL":
name = element.Name if element.Name else ""
return normalize_name(name)
elif object_type == "ORGANIZATION":
name = element.Name if element.Name else ""
return normalize_name(name)
elif object_type == "APPLICATION":
name = element.ApplicationFullName if element.ApplicationFullName else ""
return normalize_name(name)
elif object_type == "PERSON":
ident = element.Identification if element.Identification else ""
return normalize_name(ident)
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"{normalize_name(person_id)}|{normalize_name(org_name)}"
else:
assert_never(object_type)
def get_element_name(element: ifcopenshell.entity_instance) -> str:
"""Get the actual name/identifier from element for sorting purposes"""
if object_type == "STYLE":
return element.Name if element.Name else ""
elif object_type == "MATERIAL":
@@ -209,7 +245,7 @@ class Debug(bonsai.core.tool.Debug):
hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
if by_name_or_identification_only:
# Group by name/identifier only
# Group by name/identifier only (with .XXX suffix normalization)
hash_to_elements = defaultdict(list)
for element in elements:
name_key = get_name_key(element)
@@ -217,6 +253,18 @@ class Debug(bonsai.core.tool.Debug):
if not name_key:
continue
hash_to_elements[name_key].append(element)
# Sort elements within each group to keep the one without suffix (or lowest suffix)
for name_key in hash_to_elements:
# Sort by: 1) prefer names without .XXX suffix, 2) then by original name
def sort_key(el):
name = get_element_name(el)
# Check if name has .XXX suffix
has_suffix = bool(re.match(r"^.+\.\d+$", name))
# Return tuple: (has_suffix, name) - sort by no suffix first, then alphabetically
return (has_suffix, name)
hash_to_elements[name_key].sort(key=sort_key)
else:
# Group by full hash
hash_to_elements = defaultdict(list)