From 072b9fd14776e3e308d0077b0dd1ea6ec71d2608 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 8 Jun 2024 22:26:41 +1000 Subject: [PATCH] Start simplifying collection tree to only place items in their container and only one level deep. This should heavily simplify the complex logic around blender collections and give freedom to the user to do whatever they want. --- src/blenderbim/blenderbim/bim/import_ifc.py | 150 +------------ .../blenderbim/bim/module/project/prop.py | 9 - .../blenderbim/bim/module/project/ui.py | 1 - src/blenderbim/blenderbim/tool/collector.py | 204 ++++-------------- 4 files changed, 51 insertions(+), 313 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 59707e65f6..08b9f565bf 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -1602,108 +1602,13 @@ class IfcImporter: obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"] def create_collections(self) -> None: - self.create_spatial_decomposition_collections() - if self.ifc_import_settings.collection_mode == "DECOMPOSITION": - self.create_aggregate_and_nest_collections() - elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION": - pass - - def create_spatial_decomposition_collections(self) -> None: - for rel_aggregate in self.project["ifc"].IsDecomposedBy or []: - self.create_spatial_decomposition_collection(self.project["blender"], rel_aggregate.RelatedObjects) - - # Invalid IFCs may have orphaned spatial structure elements. - orphaned_spaces = [e for e in self.spatial_elements if e.GlobalId not in self.collections] - while orphaned_spaces: - self.create_spatial_decomposition_collection(self.project["blender"], orphaned_spaces) - orphaned_spaces = [e for e in self.spatial_elements if e.GlobalId not in self.collections] - + for element in self.spatial_elements: + collection = bpy.data.collections.new(tool.Loader.get_name(element)) + self.collections[element.GlobalId] = collection + self.project["blender"].children.link(collection) tool.Loader.create_project_collection("Views") self.type_collection = tool.Loader.create_project_collection("Types") - def create_spatial_decomposition_collection( - self, parent: Union[bpy.types.Collection, bpy.types.Object], related_objects: list[ifcopenshell.entity_instance] - ) -> None: - for element in related_objects: - if element not in self.spatial_elements: - continue - is_existing = False - if self.has_existing_project: - obj = tool.Ifc.get_object(element) - if obj: - is_existing = True - collection = obj.BIMObjectProperties.collection - self.collections[element.GlobalId] = collection - if not is_existing: - collection = bpy.data.collections.new(tool.Loader.get_name(element)) - self.collections[element.GlobalId] = collection - parent.children.link(collection) - if element.IsDecomposedBy: - for rel_aggregate in element.IsDecomposedBy: - self.create_spatial_decomposition_collection(collection, rel_aggregate.RelatedObjects) - - def create_aggregate_and_nest_collections(self): - if self.ifc_import_settings.has_filter: - rel_aggregates = set() - for element in self.elements: - if decomposed_by := element.IsDecomposedBy: - rel_aggregates.add(decomposed_by[0]) - elif decomposes := element.Decomposes: - rel_aggregates.add(decomposes[0]) - elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy - if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None): - rel_aggregates.add(nested_by[0]) - elif nests := getattr(element, "Nests", []): - rel_aggregates.add(nests[0]) - elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3": - rel_aggregates.add(element.AdheresToElement[0]) - else: - rel_aggregates = [ - r - for r in self.file.by_type("IfcRelAggregates") - if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType") - ] + [ - r - for r in self.file.by_type("IfcRelNests") - if ( - (relating_obj := r.RelatingObject).is_a("IfcElement") - or relating_obj.is_a("IfcElementType") - or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid")) - ) - and [e for e in r.RelatedObjects if not e.is_a("IfcPort")] - ] - if self.file.schema == "IFC4X3": - rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")] - - if len(rel_aggregates) > 10000: - # More than 10,000 collections makes Blender unhappy - print("Skipping aggregate collections for performance.") - self.ifc_import_settings.collection_mode = "SPATIAL_DECOMPOSITION" - return - - aggregates: dict[str, dict] = {} - for rel_aggregate in rel_aggregates: - element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr( - rel_aggregate, "RelatingElement" - ) - collection = bpy.data.collections.new(tool.Loader.get_name(element)) - aggregates[element.GlobalId] = {"element": element, "collection": collection} - self.collections[element.GlobalId] = collection - - for global_id, aggregate in aggregates.items(): - parent = ifcopenshell.util.element.get_aggregate(aggregate["element"]) - if parent: - self.collections[parent.GlobalId].children.link(aggregate["collection"]) - continue - parent = ifcopenshell.util.element.get_container(aggregate["element"]) - if parent: - self.collections[parent.GlobalId].children.link(aggregate["collection"]) - continue - if aggregate["element"].is_a("IfcElementType"): - self.type_collection.children.link(aggregate["collection"]) - continue - self.project["blender"].children.link(aggregate["collection"]) - def create_materials(self) -> None: for material in self.file.by_type("IfcMaterial"): self.create_material(material) @@ -1763,14 +1668,6 @@ class IfcImporter: self.place_object_in_collection(self.file.by_id(ifc_definition_id), obj) def place_object_in_collection(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None: - if self.ifc_import_settings.collection_mode == "DECOMPOSITION": - self.place_object_in_decomposition_collection(element, obj) - elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION": - self.place_object_in_spatial_decomposition_collection(element, obj) - - def place_object_in_decomposition_collection( - self, element: ifcopenshell.entity_instance, obj: bpy.types.Object - ) -> None: if element.is_a("IfcProject"): return elif element.is_a("IfcGridAxis"): @@ -1781,51 +1678,18 @@ class IfcImporter: obj.BIMObjectProperties.collection = collection collection.name = obj.name return collection.objects.link(obj) - elif getattr(element, "Decomposes", None): - aggregate = ifcopenshell.util.element.get_aggregate(element) - return self.collections[aggregate.GlobalId].objects.link(obj) - elif getattr(element, "Nests", None) and not element.is_a("IfcPort"): - nest = ifcopenshell.util.element.get_nest(element) - return self.collections[nest.GlobalId].objects.link(obj) - elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3": - adherend = element.AdheresToElement[0].RelatingElement - return self.collections[adherend.GlobalId].objects.link(obj) - - return self.place_object_in_spatial_decomposition_collection(element, obj) - - def place_object_in_spatial_decomposition_collection( - self, element: ifcopenshell.entity_instance, obj: bpy.types.Object - ) -> None: - if element.is_a("IfcProject"): - return - elif element.is_a("IfcGridAxis"): - return - elif element.GlobalId in self.collections: - collection = self.collections[element.GlobalId] - collection.BIMCollectionProperties.obj = obj - obj.BIMObjectProperties.collection = collection - return collection.objects.link(obj) elif element.is_a("IfcTypeObject"): return self.type_collection.objects.link(obj) elif element.is_a("IfcStructuralMember"): return self.structural_member_collection.objects.link(obj) elif element.is_a("IfcStructuralConnection"): return self.structural_connection_collection.objects.link(obj) + elif container := ifcopenshell.util.element.get_container(element): + self.collections[container.GlobalId].objects.link(obj) elif element.is_a("IfcAnnotation"): group = self.get_drawing_group(element) if group: return self.collections[group.GlobalId].objects.link(obj) - - container = ifcopenshell.util.element.get_container(element) - if container: - if element.is_a("IfcGrid"): # TODO: refactor into a more holistic collection mode feature - grid_collection = bpy.data.collections.get(obj.name) - if grid_collection: # Just in case we run into invalid grids from Revit - self.collections[container.GlobalId].children.link(grid_collection) - grid_collection.objects.link(obj) - else: - self.collections[container.GlobalId].objects.link(obj) - else: self.ifc_import_settings.logger.warning("Warning: this object is outside the spatial hierarchy %s", element) bpy.context.scene.collection.objects.link(obj) @@ -2068,7 +1932,6 @@ class IfcImportSettings: self.should_filter_spatial_elements = True self.should_setup_viewport_camera = True self.elements: set[ifcopenshell.entity_instance] = set() - self.collection_mode = "DECOMPOSITION" @staticmethod def factory(context=None, input_file=None, logger=None): @@ -2080,7 +1943,6 @@ class IfcImportSettings: logger = logging.getLogger("ImportIFC") settings.logger = logger settings.diff_file = scene_diff.diff_json_file - settings.collection_mode = props.collection_mode settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing settings.merge_mode = props.merge_mode settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index d56392fe31..99e9654a2f 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -125,15 +125,6 @@ class BIMProjectProperties(PropertyGroup): library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty) library_elements: CollectionProperty(name="Library Elements", type=LibraryElement) active_library_element_index: IntProperty(name="Active Library Element Index") - collection_mode: bpy.props.EnumProperty( - items=[ - ("DECOMPOSITION", "Decomposition", "Collections represent aggregates and spatial containers"), - ("SPATIAL_DECOMPOSITION", "Spatial Decomposition", "Collections represent spatial containers"), - ("IFC_CLASS", "IFC Class", "Collections represent IFC class"), - ("NONE", "None", "No collections are created"), - ], - name="Collection Mode", - ) filter_mode: bpy.props.EnumProperty( items=[ ("NONE", "None", "No filtering is performed"), diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 752f2d6eab..7a72d4bcca 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -104,7 +104,6 @@ class BIM_PT_project(Panel): def draw_load_ui(self, context): pprops = context.scene.BIMProjectProperties - prop_with_search(self.layout, pprops, "collection_mode") prop_with_search(self.layout, pprops, "filter_mode") if pprops.filter_mode in ["DECOMPOSITION", "IFC_CLASS", "IFC_TYPE"]: row = self.layout.row(align=True) diff --git a/src/blenderbim/blenderbim/tool/collector.py b/src/blenderbim/blenderbim/tool/collector.py index 9491fcccb3..425a40ecf2 100644 --- a/src/blenderbim/blenderbim/tool/collector.py +++ b/src/blenderbim/blenderbim/tool/collector.py @@ -17,13 +17,11 @@ # along with BlenderBIM Add-on. If not, see . import bpy -import ifcopenshell.util.element - import blenderbim.core.tool import blenderbim.core.spatial import blenderbim.core.aggregate import blenderbim.tool as tool -from typing import Union +import ifcopenshell.util.element class Collector(blenderbim.core.tool.Collector): @@ -33,164 +31,52 @@ class Collector(blenderbim.core.tool.Collector): and unlink them from any other""" element = tool.Ifc.get_entity(obj) - object_collection = None - collection_collection = None + if element.is_a("IfcGridAxis"): + element = (element.PartOfU or element.PartOfV or element.PartOfW)[0] - object_collection = cls._get_own_collection(element, obj) - if object_collection: - collection_collection = cls._get_collection(element, obj) - else: - object_collection = cls._get_collection(element, obj) - - # NOTE: calling `obj.users_collection` is expensive in large projects - # since it's iterating over all collections under the hood - - # ensure object is linked only to object_collection - if obj.users_collection != (object_collection,): - for collection in obj.users_collection: - collection.objects.unlink(obj) - if object_collection is not None: - object_collection.objects.link(obj) - - # ensure object_collection is linked only to collection_collection - if collection_collection and collection_collection.children.find(object_collection.name) == -1: - if bpy.context.scene.collection.children.find(object_collection.name) != -1: - bpy.context.scene.collection.children.unlink(object_collection) - for collection in bpy.data.collections: - if collection.children.find(object_collection.name) != -1: - collection.children.unlink(object_collection) - collection_collection.children.link(object_collection) - - # If an aggregate or nested host loses all its children, it no longer needs its own collection - if obj.BIMObjectProperties.collection and obj.BIMObjectProperties.collection != object_collection: - bpy.data.collections.remove(obj.BIMObjectProperties.collection) - - @classmethod - def _get_own_collection( - cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object - ) -> Union[bpy.types.Collection, None]: - """get or create own collection for the element if it's neccessary for it's type""" if element.is_a("IfcProject"): - return cls._create_own_collection(obj) - - if tool.Ifc.get_schema() == "IFC2X3": - if element.is_a("IfcSpatialStructureElement"): - return cls._create_own_collection(obj) - else: - if element.is_a("IfcSpatialStructureElement") or element.is_a("IfcExternalSpatialStructureElement"): - return cls._create_own_collection(obj) - - if element.is_a("IfcGrid"): - return cls._create_own_collection(obj) - - if element.is_a("IfcGridAxis"): - if element.PartOfU: - grid = element.PartOfU[0] - axes = "UAxes" - elif element.PartOfV: - grid = element.PartOfV[0] - axes = "VAxes" - elif element.PartOfW: - grid = element.PartOfW[0] - axes = "WAxes" - grid_obj = tool.Ifc.get_object(grid) - if grid_obj: - grid_col = cls._get_own_collection(grid, grid_obj) - axes_col = next((c for c in grid_col.children if axes in c.name), None) - return axes_col or bpy.data.collections.new(axes) - - if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": - return cls._create_own_collection(obj) - - if element.is_a("IfcStructuralMember"): - return bpy.data.collections.get("Members") or bpy.data.collections.new("Members") - - if element.is_a("IfcStructuralConnection"): - return bpy.data.collections.get("Connections") or bpy.data.collections.new("Connections") - - if getattr(element, "IsDecomposedBy", None): - return cls._create_own_collection(obj) - - if getattr(element, "IsNestedBy", None): - if any(e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")): - return cls._create_own_collection(obj) - - if getattr(element, "HasSurfaceFeatures", None): - return cls._create_own_collection(obj) - - @classmethod - def _get_collection(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> bpy.types.Collection: - """get or create collection for the element based on it's type""" - if element.is_a("IfcTypeObject"): - return cls._create_project_child_collection("Types") - - if element.is_a("IfcOpeningElement"): - return cls._create_project_child_collection("IfcOpeningElements") - - if element.is_a("IfcGridAxis"): - if element.PartOfU: - grid = element.PartOfU[0] - axes = "UAxes" - elif element.PartOfV: - grid = element.PartOfV[0] - axes = "VAxes" - elif element.PartOfW: - grid = element.PartOfW[0] - axes = "WAxes" - grid_obj = tool.Ifc.get_object(grid) - if grid_obj: - return grid_obj.BIMObjectProperties.collection - - if element.is_a("IfcAnnotation"): - if element.ObjectType == "DRAWING": - return cls._create_project_child_collection("Views") - for rel in element.HasAssignments or []: - if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING": - for related_object in rel.RelatedObjects: - if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING": - drawing_obj = tool.Ifc.get_object(related_object) - if drawing_obj: - return drawing_obj.BIMObjectProperties.collection - - if element.is_a("IfcStructuralItem"): - return cls._create_project_child_collection("StructuralItems") - - aggregate = ifcopenshell.util.element.get_aggregate(element) - if aggregate: - aggregate_obj = tool.Ifc.get_object(aggregate) - if aggregate_obj: - collection = aggregate_obj.BIMObjectProperties.collection - if collection: - return collection - - nest = ifcopenshell.util.element.get_nest(element) - if nest: - nest_obj = tool.Ifc.get_object(nest) - if nest_obj: - collection = nest_obj.BIMObjectProperties.collection - if collection: - return collection - - container = ifcopenshell.util.element.get_container(element) - if container: + if collection := cls._create_own_collection(obj): + collection.objects.link(obj) + bpy.context.scene.collection.children.link(collection) + elif element.is_a("IfcTypeProduct"): + collection = cls._create_project_child_collection("IfcTypeProduct") + collection.objects.link(obj) + elif element.is_a("IfcOpeningElement"): + collection = cls._create_project_child_collection("IfcOpeningElement") + collection.objects.link(obj) + elif element.is_a("IfcStructuralItem"): + collection = cls._create_project_child_collection("IfcStructuralItem") + collection.objects.link(obj) + elif tool.Ifc.get_schema() == "IFC2X3" and element.is_a("IfcSpatialStructureElement"): + if collection := cls._create_own_collection(obj): + collection.objects.link(obj) + project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) + project_obj.BIMObjectProperties.collection.children.link(collection) + elif tool.Ifc.get_schema() != "IFC2X3" and element.is_a("IfcSpatialElement"): + if collection := cls._create_own_collection(obj): + collection.objects.link(obj) + project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) + project_obj.BIMObjectProperties.collection.children.link(collection) + elif container := ifcopenshell.util.element.get_container(element): container_obj = tool.Ifc.get_object(container) - collection = container_obj.BIMObjectProperties.collection - if collection: - return collection - - if element.is_a("IfcSurfaceFeature") and element.file.schema == "IFC4X3": - adherend = element.AdheresToElement[0].RelatingElement - adherend_obj = tool.Ifc.get_object(adherend) - collection = adherend_obj.BIMObjectProperties.collection - if collection: - return collection - - if element.is_a("IfcProject"): - return bpy.context.scene.collection - - project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - if project_obj: - return project_obj.BIMObjectProperties.collection + container_obj.BIMObjectProperties.collection.objects.link(obj) + elif element.is_a("IfcAnnotation"): + if element.ObjectType == "DRAWING": + if collection := cls._create_own_collection(obj): + collection.objects.link(obj) + project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) + project_obj.BIMObjectProperties.collection.children.link(collection) + else: + for rel in element.HasAssignments or []: + if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING": + for related_object in rel.RelatedObjects: + if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING": + drawing_obj = tool.Ifc.get_object(related_object) + if drawing_obj: + drawing_obj.BIMObjectProperties.collection.objects.link(obj) + else: + collection = cls._create_project_child_collection("Unsorted") + collection.objects.link(obj) @classmethod def _create_project_child_collection(cls, name: str) -> bpy.types.Collection: @@ -207,7 +93,7 @@ class Collector(blenderbim.core.tool.Collector): def _create_own_collection(cls, obj: bpy.types.Object) -> bpy.types.Collection: """get or create own collection for the element""" if obj.BIMObjectProperties.collection: - return obj.BIMObjectProperties.collection + return collection = bpy.data.collections.new(obj.name) obj.BIMObjectProperties.collection = collection collection.BIMCollectionProperties.obj = obj