bim.merge_identical_objects for merging identical styles

Button location - https://i.imgur.com/3O8gHUn.png

Could also be used as a temporary workaround for #5446
This commit is contained in:
Andrej730
2024-10-03 16:15:11 +05:00
parent be9c9b2caa
commit c3e4ebd996
5 changed files with 128 additions and 2 deletions
@@ -27,6 +27,7 @@ classes = (
operator.DebugActiveDrawing,
operator.InspectFromObject,
operator.InspectFromStepId,
operator.MergeIdenticalObjects,
operator.OverrideDisplayType,
operator.ParseExpress,
operator.PipInstall,
@@ -688,6 +688,52 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.load_materials()
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_identical_objects"
bl_label = "Merge Identical Objects"
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty(
name="Object Type",
items=(
("TYPE", "Type", ""),
("PROFILE", "Profile", ""),
("STYLE", "Style", ""),
("MATERIAL", "Material", ""),
),
)
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())
else:
self.report({"ERROR"}, f"Invalid object type {object_type}.")
return {"CANCELLED"}
msg = " See system console for details." if merged else ""
self.report({"INFO"}, f"{merged} identical {object_type.lower()}s were merged.{msg}")
if merged == 0:
return
scene = context.scene
if object_type == "PROFILE":
if scene.BIMProfileProperties.is_editing:
bpy.ops.bim.load_profiles()
elif object_type == "STYLE":
if scene.BIMStylesProperties.is_editing:
bpy.ops.bim.load_styles()
elif object_type == "MATERIAL":
if scene.BIMMaterialProperties.is_editing:
bpy.ops.bim.load_materials()
class PipInstall(bpy.types.Operator):
bl_idname = "bim.pip_install"
bl_label = "Pip Install"
+4 -1
View File
@@ -525,4 +525,7 @@ 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_objects", text="Purge Unused Materials").object_type = "MATERIAL"
layout.operator("bim.purge_unused_objects", text="Purge Unused Styles").object_type = "STYLE"
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"
+56 -1
View File
@@ -17,16 +17,20 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import json
import bpy
import ifcopenshell.express
import ifcopenshell.express.schema
import ifcopenshell.express.schema_class
import ifcopenshell.util.element
import ifcopenshell.util.schema
import bonsai.core.style
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from mathutils import Vector
from typing import Iterable
from collections import defaultdict
from typing import Iterable, Literal
class Debug(bonsai.core.tool.Debug):
@@ -98,3 +102,54 @@ class Debug(bonsai.core.tool.Debug):
print(f"{class_string: <50} {unused[ifc_class]: >5}")
return sum(unused.values())
@classmethod
def merge_identical_objects(cls, object_type: Literal["style"]) -> 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))
ifc_file = tool.Ifc.get()
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)]
for element_type in element_types:
elements = ifc_file.by_type(element_type)
# 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.
if not element.Name:
continue
element_hash = get_hash(element)
hash_to_elements[element_hash].append(element)
merged_elements_names: list[str] = []
# Merge styles.
for elements in hash_to_elements.values():
if len(elements) == 1:
continue
main_element = elements[0]
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)
style_obj = tool.Ifc.get_object(style)
# Only for surface styles.
if style_obj:
assert main_style_obj
style_obj.user_remap(main_style_obj)
merged_elements_names.append(style.Name)
bonsai.core.style.remove_style(tool.Ifc, tool.Style, style, reload_styles_ui=False)
if merged_elements_names:
merged_element_types[element_type] = merged_elements_names
return merged_element_types
+21
View File
@@ -19,6 +19,7 @@
import os
import bpy
import ifcopenshell
import ifcopenshell.util.schema
import bonsai.core.tool
import bonsai.tool as tool
from test.bim.bootstrap import NewFile
@@ -66,3 +67,23 @@ class TestPurgeHdf5Cache(NewFile):
# On Unix loaded files are not locked.
paths = [loaded_file_path] if os.name == "nt" else []
assert [f for f in cache_dir.iterdir() if f.suffix == ".h5"] == paths
class TestMergeIdenticalObject(NewFile):
def test_merge_identical_styles(self):
tool.Ifc.set(ifc := ifcopenshell.file())
declaration = tool.Ifc.schema().declaration_by_name("IfcPresentationStyle")
style_types = [d.name() for d in ifcopenshell.util.schema.get_subtypes(declaration)]
for style_type in style_types:
ifc.create_entity(style_type, Name=style_type)
ifc.create_entity(style_type, Name=style_type)
ifc.create_entity(style_type, Name="NotToMerge")
if style_type == "IfcSurfaceStyle":
for style in ifc.by_type(style_type):
style.Styles = (ifc.create_entity("IfcSurfaceStyleShading"),)
elif style_type == "IfcFillAreaStyle":
for style in ifc.by_type(style_type):
style.FillStyles = (ifc.create_entity("IfcFillAreaStyleHatching"),)
merge_data = subject.merge_identical_objects("style")
assert merge_data == {style_type: [style_type] for style_type in style_types}