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
if obj.library:
continue
tool.Collector.sync(obj, skip_unlinking)
result = self.sync_object_placement(obj)
if result:
results.append(result)
+3 -1
View File
@@ -302,7 +302,9 @@ class IfcImporter:
self.setup_viewport_camera()
self.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)
bpy.context.window_manager.progress_end()
@@ -159,7 +159,6 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
if not element:
continue
tool.Collector.sync(obj)
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
current_container = ifcopenshell.util.element.get_container(element)
if current_aggregate:
@@ -122,8 +122,7 @@ class CreateProject(bpy.types.Operator):
bpy.data.meshes.remove(mesh)
for mat in bpy.data.materials:
bpy.data.materials.remove(mat)
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
core.create_project(tool.Ifc, tool.Project, tool.Spatial, schema=props.export_schema, template=template)
tool.Blender.register_toolbar()
def rollback(self, data):
+5 -2
View File
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
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():
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.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()
if template:
+6 -2
View File
@@ -67,7 +67,7 @@ def assign_class(
predefined_type: Optional[str] = None,
should_add_representation: bool = True,
ifc_representation_class: Optional[str] = None,
) -> ifcopenshell.entity_instance:
) -> Optional[ifcopenshell.entity_instance]:
"""
Args:
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
)
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)
return element
@@ -86,7 +86,6 @@ def copy_to_container(ifc, collector, spatial, obj=None, containers=None):
element = ifc.get_entity(obj)
if not element:
return
collector.sync(obj)
from_container = spatial.get_container(element)
if 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 does_type_have_representations(cls, element): 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_type(cls, element): pass
def get_object_name(cls, obj): pass
def get_object_representation(cls, obj): 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_spatial_element(cls, element): pass
def link_object_data(cls, source_obj, destination_obj): 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
@@ -851,6 +854,7 @@ class Spatial:
def import_spatial_decomposition(cls): 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_import_spatial_decomposition(cls): pass
def select_object(cls, obj): pass
def select_products(cls, products, unhide=False): pass
def set_active_object(cls, obj): pass
@@ -27,81 +27,6 @@ from typing import Union
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
def assign(cls, obj: bpy.types.Object) -> None:
"""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}
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
def get_connection_relationships(
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:
return representation.ContextOfItems
@classmethod
def is_containable(cls, element: ifcopenshell.entity_instance) -> bool:
return element.is_a("IfcElement") or element.is_a("IfcGrid")
@classmethod
def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool:
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
def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None:
destination_obj.data = source_obj.data
+23 -5
View File
@@ -30,8 +30,8 @@ import blenderbim.tool as tool
import json
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon, MultiPolygon
from typing import Generator
from shapely import Polygon
from typing import Generator, Optional
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
)
@classmethod
def run_spatial_import_spatial_decomposition(cls):
return blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
@classmethod
def select_object(cls, obj):
obj.select_set(True)
@@ -445,7 +449,6 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod
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
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector())
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")
prefix = getattr(project_unit, "Prefix", None)
converted_tolerance = ifcopenshell.util.unit.convert(
return ifcopenshell.util.unit.convert(
value=tolerance,
from_prefix=None,
from_unit="METRE",
to_prefix=prefix,
to_unit=project_unit.Name,
)
return tolerance
@classmethod
def get_purged_inner_holes_poly(cls, union_geom, min_area):
@@ -830,3 +832,19 @@ class Spatial(blenderbim.core.tool.Spatial):
@classmethod
def set_default_container(cls, container):
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:
def test_run(self, ifc, collector, spatial):
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")
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")
@@ -97,7 +96,6 @@ class TestCopyToContainer:
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")
collector.sync("obj").should_be_called()
spatial.get_container("element").should_be_called().will_return(None)
spatial.get_object_matrix("obj").should_be_called().will_return("matrix")