mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-28 03:05:33 +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):
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user