mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 22:33:33 +00:00
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:
@@ -818,7 +818,12 @@ 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 = "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"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
@@ -145,10 +146,21 @@ class Debug(bonsai.core.tool.Debug):
|
|||||||
Args:
|
Args:
|
||||||
object_type: The type of object to merge
|
object_type: The type of object to merge
|
||||||
by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
|
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, uses Identification. For APPLICATION, uses ApplicationFullName.
|
||||||
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
|
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:
|
def get_hash(element: ifcopenshell.entity_instance) -> int:
|
||||||
data = element.get_info_2(include_identifier=False, recursive=True)
|
data = element.get_info_2(include_identifier=False, recursive=True)
|
||||||
if object_type == "APPLICATION":
|
if object_type == "APPLICATION":
|
||||||
@@ -161,6 +173,30 @@ class Debug(bonsai.core.tool.Debug):
|
|||||||
|
|
||||||
def get_name_key(element: ifcopenshell.entity_instance) -> str:
|
def get_name_key(element: ifcopenshell.entity_instance) -> str:
|
||||||
"""Get key based on name/identifier attribute for the given object type"""
|
"""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":
|
if object_type == "STYLE":
|
||||||
return element.Name if element.Name else ""
|
return element.Name if element.Name else ""
|
||||||
elif object_type == "MATERIAL":
|
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]]
|
hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
|
||||||
|
|
||||||
if by_name_or_identification_only:
|
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)
|
hash_to_elements = defaultdict(list)
|
||||||
for element in elements:
|
for element in elements:
|
||||||
name_key = get_name_key(element)
|
name_key = get_name_key(element)
|
||||||
@@ -217,6 +253,18 @@ class Debug(bonsai.core.tool.Debug):
|
|||||||
if not name_key:
|
if not name_key:
|
||||||
continue
|
continue
|
||||||
hash_to_elements[name_key].append(element)
|
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:
|
else:
|
||||||
# Group by full hash
|
# Group by full hash
|
||||||
hash_to_elements = defaultdict(list)
|
hash_to_elements = defaultdict(list)
|
||||||
|
|||||||
Reference in New Issue
Block a user