diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index af5c841761..3b801935ed 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -42,7 +42,7 @@ from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from bonsai import get_debug_info, format_debug_info from bonsai.bim.ifc import IfcStore -from typing import get_args, Union, Any, TYPE_CHECKING +from typing import get_args, Union, Any, TYPE_CHECKING, Literal, get_args if TYPE_CHECKING: from bonsai.bim.prop import Attribute @@ -739,6 +739,9 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHe tool.Ifc.get().write(self.filepath) +PurgeObjectType = Literal["TYPE", "PROFILE", "STYLE", "MATERIAL", "ORGANIZATION", "APPLICATION"] + + class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.purge_unused_objects" bl_label = "Purge Unused Objects" @@ -793,23 +796,21 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): bl_description = "For materials currently only IfcMaterials are supported" bl_options = {"REGISTER", "UNDO"} - object_type: bpy.props.EnumProperty( + object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] name="Object Type", - items=( - ("TYPE", "Type", ""), - ("PROFILE", "Profile", ""), - ("STYLE", "Style", ""), - ("MATERIAL", "Material", ""), - ), + items=((s, s.capitalize(), "") for s in get_args(PurgeObjectType)), ) + if TYPE_CHECKING: + object_type: PurgeObjectType + def _execute(self, context): object_type: str = self.object_type - if object_type in ("STYLE", "MATERIAL"): - merged_data = tool.Debug.merge_identical_objects(object_type) - else: - self.report({"ERROR"}, f"Invalid object type {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) plural_object_type = f"{object_type.lower()}s" if merged_data: for element_type, element_names in merged_data.items(): diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index f066b0c51d..836b9cc978 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -634,11 +634,22 @@ class BIM_PT_purge(Panel): layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE" layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE" layout.operator("bim.purge_unused_openings", text="Purge Unused Openings in Selected Objects") + row = layout.row(align=True) row.label(text="Materials: ") row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = "MATERIAL" row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = "MATERIAL" + row = layout.row(align=True) + row.label(text="Styles: ") row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = "STYLE" row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = "STYLE" + + row = layout.row(align=True) + row.label(text="Organizations: ") + row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = "ORGANIZATION" + + row = layout.row(align=True) + row.label(text="Applications: ") + row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = "APPLICATION" diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 5fe25c4681..5dd09b1780 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -22,6 +22,7 @@ import json import bpy import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.api.material +import ifcopenshell.api.owner import ifcopenshell.express import ifcopenshell.util.element import ifcopenshell.util.schema @@ -31,7 +32,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 +from typing import Literal, TYPE_CHECKING, assert_never from collections.abc import Iterable if TYPE_CHECKING: @@ -122,24 +123,38 @@ class Debug(bonsai.core.tool.Debug): return sum(unused.values()) @classmethod - def merge_identical_objects(cls, object_type: Literal["STYLE", "MATERIAL"]) -> dict[str, list[str]]: + def merge_identical_objects( + cls, + object_type: Literal["STYLE", "MATERIAL", "ORGANIZATION", "APPLICATION"], + ) -> dict[str, list[str]]: """Merge identical objects. Note that Styles UI (or other UI) should be updated manually after using this method. """ def get_hash(element: ifcopenshell.entity_instance) -> int: - return hash(json.dumps(element.get_info_2(include_identifier=False, recursive=True), sort_keys=True)) + data = element.get_info_2(include_identifier=False, recursive=True) + if object_type == "APPLICATION": + # To avoid disruption let user merge organizations separately. + data["ApplicationDeveloper"] = element.ApplicationDeveloper.id() + return hash(json.dumps(data, sort_keys=True)) ifc_file = tool.Ifc.get() merged_element_types: dict[str, list[str]] = {} if object_type == "STYLE": - declaration = tool.Ifc.schema().declaration_by_name("IfcPresentationStyle") + declaration = tool.Ifc.schema().declaration_by_name("IfcPresentationStyle").as_entity() + assert declaration element_types = [e.name() for e in ifcopenshell.util.schema.get_subtypes(declaration)] elif object_type == "MATERIAL": # TODO: support other material types. element_types = ["IfcMaterial"] + elif object_type == "ORGANIZATION": + element_types = ["IfcOrganization"] + elif object_type == "APPLICATION": + element_types = ["IfcApplication"] + else: + assert_never(object_type) for element_type in element_types: elements = ifc_file.by_type(element_type, include_subtypes=False) @@ -148,7 +163,8 @@ class Debug(bonsai.core.tool.Debug): 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 - if object_type != "STYLE" and not element.Name: + not_optional_name = ("APPLICATION", "ORGANIZATION") + if object_type != "STYLE" and object_type not in not_optional_name and not element.Name: continue element_hash = get_hash(element) hash_to_elements[element_hash].append(element) @@ -178,6 +194,21 @@ class Debug(bonsai.core.tool.Debug): merged_elements_names.append(material.Name) ifcopenshell.api.material.remove_material(ifc_file, material) + elif object_type == "ORGANIZATION": + for organization in elements[1:]: + ifcopenshell.util.element.replace_element(organization, main_element) + merged_elements_names.append(organization.Name) + ifcopenshell.api.owner.remove_organisation(ifc_file, organization) + + elif object_type == "APPLICATION": + for application in elements[1:]: + ifcopenshell.util.element.replace_element(application, main_element) + merged_elements_names.append(application.ApplicationFullName) + ifcopenshell.api.owner.remove_application(ifc_file, application) + + else: + assert_never(object_type) + if merged_elements_names: merged_element_types[element_type] = merged_elements_names return merged_element_types