mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 22:22:26 +00:00
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:
@@ -27,6 +27,7 @@ classes = (
|
|||||||
operator.DebugActiveDrawing,
|
operator.DebugActiveDrawing,
|
||||||
operator.InspectFromObject,
|
operator.InspectFromObject,
|
||||||
operator.InspectFromStepId,
|
operator.InspectFromStepId,
|
||||||
|
operator.MergeIdenticalObjects,
|
||||||
operator.OverrideDisplayType,
|
operator.OverrideDisplayType,
|
||||||
operator.ParseExpress,
|
operator.ParseExpress,
|
||||||
operator.PipInstall,
|
operator.PipInstall,
|
||||||
|
|||||||
@@ -688,6 +688,52 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bpy.ops.bim.load_materials()
|
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):
|
class PipInstall(bpy.types.Operator):
|
||||||
bl_idname = "bim.pip_install"
|
bl_idname = "bim.pip_install"
|
||||||
bl_label = "Pip Install"
|
bl_label = "Pip Install"
|
||||||
|
|||||||
@@ -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 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 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 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"
|
||||||
|
|||||||
@@ -17,16 +17,20 @@
|
|||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell.express
|
import ifcopenshell.express
|
||||||
import ifcopenshell.express.schema
|
import ifcopenshell.express.schema
|
||||||
import ifcopenshell.express.schema_class
|
import ifcopenshell.express.schema_class
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
|
import ifcopenshell.util.schema
|
||||||
|
import bonsai.core.style
|
||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
import bonsai.tool as tool
|
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 typing import Iterable
|
from collections import defaultdict
|
||||||
|
from typing import Iterable, Literal
|
||||||
|
|
||||||
|
|
||||||
class Debug(bonsai.core.tool.Debug):
|
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}")
|
print(f"{class_string: <50} {unused[ifc_class]: >5}")
|
||||||
|
|
||||||
return sum(unused.values())
|
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
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import os
|
import os
|
||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
|
import ifcopenshell.util.schema
|
||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
from test.bim.bootstrap import NewFile
|
from test.bim.bootstrap import NewFile
|
||||||
@@ -66,3 +67,23 @@ class TestPurgeHdf5Cache(NewFile):
|
|||||||
# On Unix loaded files are not locked.
|
# On Unix loaded files are not locked.
|
||||||
paths = [loaded_file_path] if os.name == "nt" else []
|
paths = [loaded_file_path] if os.name == "nt" else []
|
||||||
assert [f for f in cache_dir.iterdir() if f.suffix == ".h5"] == paths
|
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user