mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
Merge entities, if match on name or identification attribute only.
This commit is contained in:
@@ -818,7 +818,7 @@ 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 = "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"}
|
||||
|
||||
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)),
|
||||
)
|
||||
|
||||
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:
|
||||
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):
|
||||
object_type: str = self.object_type
|
||||
if object_type in ("PROFILE", "TYPE"):
|
||||
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
|
||||
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"
|
||||
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():
|
||||
print(f"- {element_type}:")
|
||||
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())
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
@@ -653,4 +653,5 @@ class BIM_PT_purge(Panel):
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
|
||||
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
|
||||
|
||||
@@ -33,7 +33,7 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from mathutils import Vector
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -136,10 +136,17 @@ class Debug(bonsai.core.tool.Debug):
|
||||
"PERSON",
|
||||
"PERSON_AND_ORGANIZATION",
|
||||
],
|
||||
by_name_or_identification_only: bool = False,
|
||||
) -> dict[str, list[str]]:
|
||||
"""Merge identical objects.
|
||||
|
||||
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:
|
||||
@@ -152,6 +159,25 @@ class Debug(bonsai.core.tool.Debug):
|
||||
data["TheOrganization"] = element.TheOrganization.id()
|
||||
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()
|
||||
merged_element_types: dict[str, list[str]] = {}
|
||||
|
||||
@@ -179,22 +205,35 @@ class Debug(bonsai.core.tool.Debug):
|
||||
for element_type in element_types:
|
||||
elements = ifc_file.by_type(element_type, include_subtypes=False)
|
||||
|
||||
# Calculate hashes.
|
||||
hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list)
|
||||
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)
|
||||
# Calculate hashes or name keys.
|
||||
hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
|
||||
|
||||
if by_name_or_identification_only:
|
||||
# Group by name/identifier only
|
||||
hash_to_elements = defaultdict(list)
|
||||
for element in elements:
|
||||
name_key = get_name_key(element)
|
||||
# Skip elements without a valid identifier
|
||||
if not name_key:
|
||||
continue
|
||||
hash_to_elements[name_key].append(element)
|
||||
else:
|
||||
# Group by full hash
|
||||
hash_to_elements = defaultdict(list)
|
||||
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] = []
|
||||
# Merge elements.
|
||||
|
||||
Reference in New Issue
Block a user