mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-08 17:01:40 +00:00
Bonsai - merge identical IfcOrganizations, IfcApplications
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 😅
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user