WARNING: Remove collection syncing. The outliner no longer is used to change spatial hierarchy.

This is a BREAKING change (on purpose). If you want to change containment / aggregation, use the UI panels, not drag and drop on the outliner.
This commit is contained in:
Dion Moult
2024-06-07 22:55:11 +10:00
parent db98f13982
commit e3cb811225
12 changed files with 62 additions and 92 deletions
@@ -99,7 +99,6 @@ class IfcExporter:
continue continue
if obj.library: if obj.library:
continue continue
tool.Collector.sync(obj, skip_unlinking)
result = self.sync_object_placement(obj) result = self.sync_object_placement(obj)
if result: if result:
results.append(result) results.append(result)
+3 -1
View File
@@ -302,7 +302,9 @@ class IfcImporter:
self.setup_viewport_camera() self.setup_viewport_camera()
self.setup_arrays() self.setup_arrays()
self.profile_code("Setup arrays") self.profile_code("Setup arrays")
blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial) tool.Spatial.run_spatial_import_spatial_decomposition()
if default_container := tool.Spatial.guess_default_container():
tool.Spatial.set_default_container(default_container)
self.update_progress(100) self.update_progress(100)
bpy.context.window_manager.progress_end() bpy.context.window_manager.progress_end()
@@ -159,7 +159,6 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
if not element: if not element:
continue continue
tool.Collector.sync(obj)
current_aggregate = ifcopenshell.util.element.get_aggregate(element) current_aggregate = ifcopenshell.util.element.get_aggregate(element)
current_container = ifcopenshell.util.element.get_container(element) current_container = ifcopenshell.util.element.get_container(element)
if current_aggregate: if current_aggregate:
@@ -122,8 +122,7 @@ class CreateProject(bpy.types.Operator):
bpy.data.meshes.remove(mesh) bpy.data.meshes.remove(mesh)
for mat in bpy.data.materials: for mat in bpy.data.materials:
bpy.data.materials.remove(mat) bpy.data.materials.remove(mat)
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template) core.create_project(tool.Ifc, tool.Project, tool.Spatial, schema=props.export_schema, template=template)
blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
tool.Blender.register_toolbar() tool.Blender.register_toolbar()
def rollback(self, data): def rollback(self, data):
+5 -2
View File
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
import blenderbim.tool as tool import blenderbim.tool as tool
def create_project(ifc: tool.Ifc, project: tool.Project, schema: str, template: Optional[str] = None) -> None: def create_project(ifc: tool.Ifc, project: tool.Project, spatial: tool.Spatial, schema: str, template: Optional[str] = None) -> None:
if ifc.get(): if ifc.get():
return return
@@ -90,7 +90,10 @@ def create_project(ifc: tool.Ifc, project: tool.Project, schema: str, template:
project.run_aggregate_assign_object(relating_obj=building, related_obj=storey) project.run_aggregate_assign_object(relating_obj=building, related_obj=storey)
project.set_context(body) project.set_context(body)
project.set_active_spatial_element(storey) spatial.run_spatial_import_spatial_decomposition()
if default_container := spatial.guess_default_container():
spatial.set_default_container(default_container)
project.create_project_collections() project.create_project_collections()
if template: if template:
+6 -2
View File
@@ -67,7 +67,7 @@ def assign_class(
predefined_type: Optional[str] = None, predefined_type: Optional[str] = None,
should_add_representation: bool = True, should_add_representation: bool = True,
ifc_representation_class: Optional[str] = None, ifc_representation_class: Optional[str] = None,
) -> ifcopenshell.entity_instance: ) -> Optional[ifcopenshell.entity_instance]:
""" """
Args: Args:
context: is not optional if `should_add_representation` is True context: is not optional if `should_add_representation` is True
@@ -86,6 +86,10 @@ def assign_class(
obj=obj, context=context, ifc_representation_class=ifc_representation_class, profile_set_usage=None obj=obj, context=context, ifc_representation_class=ifc_representation_class, profile_set_usage=None
) )
collector.sync(obj) if default_container := root.get_default_container():
if root.is_spatial_element(element):
ifc.run("aggregate.assign_object", products=[element], relating_object=default_container)
elif root.is_containable(element):
ifc.run("spatial.assign_container", products=[element], relating_structure=default_container)
collector.assign(obj) collector.assign(obj)
return element return element
@@ -86,7 +86,6 @@ def copy_to_container(ifc, collector, spatial, obj=None, containers=None):
element = ifc.get_entity(obj) element = ifc.get_entity(obj)
if not element: if not element:
return return
collector.sync(obj)
from_container = spatial.get_container(element) from_container = spatial.get_container(element)
if from_container: if from_container:
matrix = spatial.get_relative_object_matrix(obj, ifc.get_object(from_container)) matrix = spatial.get_relative_object_matrix(obj, ifc.get_object(from_container))
+4
View File
@@ -689,12 +689,15 @@ class Root:
def copy_representation(cls, source, dest): pass def copy_representation(cls, source, dest): pass
def does_type_have_representations(cls, element): pass def does_type_have_representations(cls, element): pass
def get_decomposition_relationships(cls, objs): pass def get_decomposition_relationships(cls, objs): pass
def get_default_container(cls): pass
def get_element_representation(cls, element, context): pass def get_element_representation(cls, element, context): pass
def get_element_type(cls, element): pass def get_element_type(cls, element): pass
def get_object_name(cls, obj): pass def get_object_name(cls, obj): pass
def get_object_representation(cls, obj): pass def get_object_representation(cls, obj): pass
def get_representation_context(cls, representation): pass def get_representation_context(cls, representation): pass
def is_containable(cls, element): pass
def is_element_a(cls, element, ifc_class): pass def is_element_a(cls, element, ifc_class): pass
def is_spatial_element(cls, element): pass
def link_object_data(cls, source_obj, destination_obj): pass def link_object_data(cls, source_obj, destination_obj): pass
def recreate_decompositions(cls, relationships, old_to_new): pass def recreate_decompositions(cls, relationships, old_to_new): pass
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
@@ -851,6 +854,7 @@ class Spatial:
def import_spatial_decomposition(cls): pass def import_spatial_decomposition(cls): pass
def run_root_copy_class(cls, obj=None): pass def run_root_copy_class(cls, obj=None): pass
def run_spatial_assign_container(cls, structure_obj=None, element_obj=None): pass def run_spatial_assign_container(cls, structure_obj=None, element_obj=None): pass
def run_spatial_import_spatial_decomposition(cls): pass
def select_object(cls, obj): pass def select_object(cls, obj): pass
def select_products(cls, products, unhide=False): pass def select_products(cls, products, unhide=False): pass
def set_active_object(cls, obj): pass def set_active_object(cls, obj): pass
@@ -27,81 +27,6 @@ from typing import Union
class Collector(blenderbim.core.tool.Collector): class Collector(blenderbim.core.tool.Collector):
@classmethod
def sync(cls, obj: bpy.types.Object, skip_unlinking=False) -> None:
"""Sync object IFC state (assigned containter / aggregate) with the collection it's currently in.
Then subsequently run `Collector.assign` (if state has changed)
to link them to collections / unlink from anything unrelated collections.
If `skip_unlinking` is `True` then method won't try to assign parent object
if it's already assigned in IFC saving some time.
But it has a downside not unlinking object from unrelated collections.
"""
# This is the reverse of assign. It reads the Blender collection and figures out its IFC hierarchy
element = tool.Ifc.get_entity(obj)
if (
not element
or element.is_a("IfcProject")
or element.is_a("IfcGridAxis")
or element.is_a("IfcOpeningElement")
):
return
if not obj.users_collection:
return
# create related collections
cls._get_own_collection(element, obj)
cls._get_collection(element, obj)
parent_collection = None
for collection in obj.users_collection:
if parent_collection:
break
# skip Types and non-BIM collections
if not collection.BIMCollectionProperties.obj:
continue
# for objects that own collections we search for the first parent collection
if collection.BIMCollectionProperties.obj == obj:
collection_name = collection.name
for bpy_collection in bpy.data.collections:
if bpy_collection.children.get(collection_name) and bpy_collection.BIMCollectionProperties.obj:
parent_collection = bpy_collection
parent_obj = bpy_collection.BIMCollectionProperties.obj
break
else:
parent_collection = collection
parent_obj = collection.BIMCollectionProperties.obj
if not parent_collection:
return
parent = tool.Ifc.get_entity(parent_obj)
if skip_unlinking:
previous_parent = ifcopenshell.util.element.get_container(
element, should_get_direct=True
) or ifcopenshell.util.element.get_aggregate(element)
if parent == previous_parent:
return
if parent:
# This is lazy, but works. One of these will succeed, the other will fail silently.
blenderbim.core.spatial.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj
)
# NOTE: won't allow assigning IfcElements to the IfcProject directly
# and some elements might get missing in other viewers if they're don't support displaying
# elements without hierarchy
try:
blenderbim.core.aggregate.assign_object(
tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj
)
except blenderbim.core.aggregate.IncompatibleAggregateError:
pass
@classmethod @classmethod
def assign(cls, obj: bpy.types.Object) -> None: def assign(cls, obj: bpy.types.Object) -> None:
"""link object and it's owned collection to the proper collection """link object and it's owned collection to the proper collection
+20
View File
@@ -98,6 +98,16 @@ class Root(blenderbim.core.tool.Root):
relationships[element] = {"type": "fill", "element": building} relationships[element] = {"type": "fill", "element": building}
return relationships return relationships
@classmethod
def get_default_container(cls) -> Optional[ifcopenshell.entity_instance]:
props = bpy.context.scene.BIMSpatialDecompositionProperties
if container := props.default_container:
try:
return tool.Ifc.get().by_id(container)
except:
props.default_container = 0
return None
@classmethod @classmethod
def get_connection_relationships( def get_connection_relationships(
cls, objs: list[bpy.types.Object] cls, objs: list[bpy.types.Object]
@@ -162,10 +172,20 @@ class Root(blenderbim.core.tool.Root):
def get_representation_context(cls, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: def get_representation_context(cls, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
return representation.ContextOfItems return representation.ContextOfItems
@classmethod
def is_containable(cls, element: ifcopenshell.entity_instance) -> bool:
return element.is_a("IfcElement") or element.is_a("IfcGrid")
@classmethod @classmethod
def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool: def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool:
return element.is_a(ifc_class) return element.is_a(ifc_class)
@classmethod
def is_spatial_element(cls, element: ifcopenshell.entity_instance) -> bool:
if tool.Ifc.get().schema == "IFC2X3":
return element.is_a("IfcSpatialStructureElement")
return element.is_a("IfcSpatialElement")
@classmethod @classmethod
def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None: def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None:
destination_obj.data = source_obj.data destination_obj.data = source_obj.data
+23 -5
View File
@@ -30,8 +30,8 @@ import blenderbim.tool as tool
import json import json
from math import pi from math import pi
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from shapely import Polygon, MultiPolygon from shapely import Polygon
from typing import Generator from typing import Generator, Optional
class Spatial(blenderbim.core.tool.Spatial): class Spatial(blenderbim.core.tool.Spatial):
@@ -134,6 +134,10 @@ class Spatial(blenderbim.core.tool.Spatial):
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=structure_obj, element_obj=element_obj tool.Ifc, tool.Collector, tool.Spatial, structure_obj=structure_obj, element_obj=element_obj
) )
@classmethod
def run_spatial_import_spatial_decomposition(cls):
return blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
@classmethod @classmethod
def select_object(cls, obj): def select_object(cls, obj):
obj.select_set(True) obj.select_set(True)
@@ -445,7 +449,6 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod @classmethod
def get_x_y_z_h_mat_from_active_obj(cls, active_obj): def get_x_y_z_h_mat_from_active_obj(cls, active_obj):
element = tool.Ifc.get_entity(active_obj)
mat = active_obj.matrix_world mat = active_obj.matrix_world
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector()) local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center global_bbox_center = mat @ local_bbox_center
@@ -518,14 +521,13 @@ class Spatial(blenderbim.core.tool.Spatial):
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT") project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
prefix = getattr(project_unit, "Prefix", None) prefix = getattr(project_unit, "Prefix", None)
converted_tolerance = ifcopenshell.util.unit.convert( return ifcopenshell.util.unit.convert(
value=tolerance, value=tolerance,
from_prefix=None, from_prefix=None,
from_unit="METRE", from_unit="METRE",
to_prefix=prefix, to_prefix=prefix,
to_unit=project_unit.Name, to_unit=project_unit.Name,
) )
return tolerance
@classmethod @classmethod
def get_purged_inner_holes_poly(cls, union_geom, min_area): def get_purged_inner_holes_poly(cls, union_geom, min_area):
@@ -830,3 +832,19 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod @classmethod
def set_default_container(cls, container): def set_default_container(cls, container):
bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id() bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id()
@classmethod
def guess_default_container(cls) -> Optional[ifcopenshell.entity_instance]:
project = tool.Ifc.get().by_type("IfcProject")[0]
subelement = None
# We try to priorise the first Site > Building > Storey as a convention for vertical projects
for subelement in ifcopenshell.util.element.get_parts(project):
if subelement.is_a("IfcSite"):
for subelement2 in ifcopenshell.util.element.get_parts(subelement):
if subelement2.is_a("IfcBuilding"):
for subelement3 in ifcopenshell.util.element.get_parts(subelement2):
if subelement3.is_a("IfcBuildingStorey"):
return subelement3
if subelement:
return subelement
return None
-2
View File
@@ -80,7 +80,6 @@ class TestRemoveContainer:
class TestCopyToContainer: class TestCopyToContainer:
def test_run(self, ifc, collector, spatial): def test_run(self, ifc, collector, spatial):
ifc.get_entity("obj").should_be_called().will_return("element") ifc.get_entity("obj").should_be_called().will_return("element")
collector.sync("obj").should_be_called()
spatial.get_container("element").should_be_called().will_return("container") spatial.get_container("element").should_be_called().will_return("container")
ifc.get_object("container").should_be_called().will_return("container_obj") ifc.get_object("container").should_be_called().will_return("container_obj")
spatial.get_relative_object_matrix("obj", "container_obj").should_be_called().will_return("matrix") spatial.get_relative_object_matrix("obj", "container_obj").should_be_called().will_return("matrix")
@@ -97,7 +96,6 @@ class TestCopyToContainer:
def test_using_an_absolute_matrix_if_there_is_no_from_container(self, ifc, collector, spatial): def test_using_an_absolute_matrix_if_there_is_no_from_container(self, ifc, collector, spatial):
ifc.get_entity("obj").should_be_called().will_return("element") ifc.get_entity("obj").should_be_called().will_return("element")
collector.sync("obj").should_be_called()
spatial.get_container("element").should_be_called().will_return(None) spatial.get_container("element").should_be_called().will_return(None)
spatial.get_object_matrix("obj").should_be_called().will_return("matrix") spatial.get_object_matrix("obj").should_be_called().will_return("matrix")