From 106323847936d80e300825261d85debcccd791d5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 17 Jun 2025 17:23:47 +0500 Subject: [PATCH] Bonsai - merge identical IfcOrganizations, IfcApplications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added an utility to merge organizations and application that have exactly the same data. Noticed working with different files that sometimes they have these data duplicated - I guess reasons vary (revit exporter, our bug - c58b84d), but it's nice to be able to clean up redundant data. Though still need to add some UI to change IfcApplications, so user could also get rid of them in case if they have small differences. Feature location - https://files.catbox.moe/pwymx1.png @theoryshaw in file from #6784 there were 2481 identical organizations and 1234 identical applications 😅 --- .../bonsai/bim/module/debug/operator.py | 25 +++++------ src/bonsai/bonsai/bim/module/project/ui.py | 11 +++++ src/bonsai/bonsai/tool/debug.py | 41 ++++++++++++++++--- 3 files changed, 60 insertions(+), 17 deletions(-) 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