From e3cb811225d232cd85b83af9e4da08bb9b770f5a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 7 Jun 2024 22:55:11 +1000 Subject: [PATCH] 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. --- src/blenderbim/blenderbim/bim/export_ifc.py | 1 - src/blenderbim/blenderbim/bim/import_ifc.py | 4 +- .../bim/module/aggregate/operator.py | 1 - .../blenderbim/bim/module/project/operator.py | 3 +- src/blenderbim/blenderbim/core/project.py | 7 +- src/blenderbim/blenderbim/core/root.py | 8 +- src/blenderbim/blenderbim/core/spatial.py | 1 - src/blenderbim/blenderbim/core/tool.py | 4 + src/blenderbim/blenderbim/tool/collector.py | 75 ------------------- src/blenderbim/blenderbim/tool/root.py | 20 +++++ src/blenderbim/blenderbim/tool/spatial.py | 28 +++++-- src/blenderbim/test/core/test_spatial.py | 2 - 12 files changed, 62 insertions(+), 92 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 94d457918d..27be04efc4 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -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) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 8c05315643..7bc9e93a72 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -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() diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index a719cea549..be53675341 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -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: diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 2e6cd2b164..718da372e8 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -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): diff --git a/src/blenderbim/blenderbim/core/project.py b/src/blenderbim/blenderbim/core/project.py index 6780843592..7ae3ea0aee 100644 --- a/src/blenderbim/blenderbim/core/project.py +++ b/src/blenderbim/blenderbim/core/project.py @@ -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: diff --git a/src/blenderbim/blenderbim/core/root.py b/src/blenderbim/blenderbim/core/root.py index 8941ea4b0e..f00198340d 100644 --- a/src/blenderbim/blenderbim/core/root.py +++ b/src/blenderbim/blenderbim/core/root.py @@ -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 diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index bc275678da..48c24e7ab3 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -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)) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index b849b7b1d3..338500ec64 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -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 diff --git a/src/blenderbim/blenderbim/tool/collector.py b/src/blenderbim/blenderbim/tool/collector.py index f8d7eaa88c..9491fcccb3 100644 --- a/src/blenderbim/blenderbim/tool/collector.py +++ b/src/blenderbim/blenderbim/tool/collector.py @@ -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 diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py index 057644e9f5..ef9566691c 100644 --- a/src/blenderbim/blenderbim/tool/root.py +++ b/src/blenderbim/blenderbim/tool/root.py @@ -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 diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py index f00aa843bd..d092d8ce76 100644 --- a/src/blenderbim/blenderbim/tool/spatial.py +++ b/src/blenderbim/blenderbim/tool/spatial.py @@ -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 diff --git a/src/blenderbim/test/core/test_spatial.py b/src/blenderbim/test/core/test_spatial.py index 9e01aefde4..8af6f8dffb 100644 --- a/src/blenderbim/test/core/test_spatial.py +++ b/src/blenderbim/test/core/test_spatial.py @@ -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")