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.
This commit is contained in:
Dion Moult
2024-06-08 22:26:41 +10:00
parent 3a1f1af803
commit 072b9fd147
4 changed files with 51 additions and 313 deletions
+6 -144
View File
@@ -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
@@ -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"),
@@ -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)
+45 -159
View File
@@ -17,13 +17,11 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
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