From 7b0a792ff71b563924a1acee0b73138532fcfa94 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 3 Oct 2024 17:09:02 +0500 Subject: [PATCH] bim.merge_identical_objects to support merging IfcMaterials --- .../bonsai/bim/module/debug/operator.py | 20 ++++++++------- src/bonsai/bonsai/bim/module/project/ui.py | 5 +++- src/bonsai/bonsai/tool/debug.py | 25 +++++++++++++------ src/bonsai/test/tool/test_debug.py | 19 +++++++++++++- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 4a276216ca..70f2ec19fa 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -691,6 +691,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_options = {"REGISTER", "UNDO"} object_type: bpy.props.EnumProperty( @@ -704,20 +705,21 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): ) def _execute(self, context): - object_type = self.object_type - if object_type == "STYLE": - merged_data = tool.Debug.merge_identical_objects("style") - if merged_data: - print("Merged styles:") - for style_type, style_names in merged_data.items(): - print(f"- {style_type}: {', '.join(style_names)}") - merged = sum(len(v) for v in merged_data.values()) + 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}.") return {"CANCELLED"} + plural_object_type = f"{object_type.lower()}s" + if merged_data: + print(f"Merged {plural_object_type}:") + for element_type, element_names in merged_data.items(): + print(f"- {element_type}: {', '.join(element_names)}") + 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 {object_type.lower()}s were merged.{msg}") + self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}") if merged == 0: return diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index a964115f6e..96576da094 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -524,7 +524,10 @@ class BIM_PT_purge(Panel): layout = self.layout 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_objects", text="Purge Unused Materials").object_type = "MATERIAL" + 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" diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 25fee59200..7f41927a8a 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -19,6 +19,7 @@ import os import json import bpy +import ifcopenshell.api.material import ifcopenshell.express import ifcopenshell.express.schema import ifcopenshell.express.schema_class @@ -104,7 +105,7 @@ class Debug(bonsai.core.tool.Debug): return sum(unused.values()) @classmethod - def merge_identical_objects(cls, object_type: Literal["style"]) -> dict[str, list[str]]: + def merge_identical_objects(cls, object_type: Literal["STYLE", "MATERIAL"]) -> dict[str, list[str]]: """Merge identical objects. Note that Styles UI (or other UI) should be updated manually after using this method. @@ -115,31 +116,35 @@ class Debug(bonsai.core.tool.Debug): return hash(json.dumps(element.get_info(include_identifier=False, recursive=True), sort_keys=True)) ifc_file = tool.Ifc.get() + merged_element_types: dict[str, list[str]] = {} - if object_type == "style": + if object_type == "STYLE": declaration = tool.Ifc.schema().declaration_by_name("IfcPresentationStyle") - merged_element_types: dict[str, list[str]] = {} 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"] + for element_type in element_types: - elements = ifc_file.by_type(element_type) + 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: - # Ignore unnamed styles as they may be not safe to merge. + # Ignore unnamed elements as they may be not safe to merge. if not element.Name: continue element_hash = get_hash(element) hash_to_elements[element_hash].append(element) merged_elements_names: list[str] = [] - # Merge styles. + # Merge elements. for elements in hash_to_elements.values(): if len(elements) == 1: continue main_element = elements[0] - if object_type == "style": + if object_type == "STYLE": main_style_obj = tool.Ifc.get_object(main_element) for style in elements[1:]: ifcopenshell.util.element.replace_element(style, main_element) @@ -151,6 +156,12 @@ class Debug(bonsai.core.tool.Debug): merged_elements_names.append(style.Name) bonsai.core.style.remove_style(tool.Ifc, tool.Style, style, reload_styles_ui=False) + elif object_type == "MATERIAL": + for material in elements[1:]: + ifcopenshell.util.element.replace_element(material, main_element) + merged_elements_names.append(material.Name) + ifcopenshell.api.material.remove_material(ifc_file, material) + if merged_elements_names: merged_element_types[element_type] = merged_elements_names return merged_element_types diff --git a/src/bonsai/test/tool/test_debug.py b/src/bonsai/test/tool/test_debug.py index cb5db3a1f7..f59f01ef60 100644 --- a/src/bonsai/test/tool/test_debug.py +++ b/src/bonsai/test/tool/test_debug.py @@ -19,6 +19,7 @@ import os import bpy import ifcopenshell +import ifcopenshell.api.style import ifcopenshell.util.schema import bonsai.core.tool import bonsai.tool as tool @@ -85,5 +86,21 @@ class TestMergeIdenticalObject(NewFile): for style in ifc.by_type(style_type): style.FillStyles = (ifc.create_entity("IfcFillAreaStyleHatching"),) - merge_data = subject.merge_identical_objects("style") + merge_data = subject.merge_identical_objects("STYLE") assert merge_data == {style_type: [style_type] for style_type in style_types} + + def test_merge_identical_materials(self): + tool.Ifc.set(ifc := ifcopenshell.file()) + context = ifc.create_entity("IfcGeometricRepresentationContext") + style = ifc.create_entity("IfcSurfaceStyle") + element_types = ["IfcMaterial"] + for element_type in element_types: + ifc.create_entity(element_type, Name=element_type, Category="Category") + ifc.create_entity(element_type, Name=element_type, Category="Category") + ifc.create_entity(element_type, Name="NotToMerge", Category="Category") + + for material in ifc.by_type(element_type): + ifcopenshell.api.style.assign_material_style(ifc, material, style, context) + + merge_data = subject.merge_identical_objects("MATERIAL") + assert merge_data == {element_type: [element_type] for element_type in element_types}