From a4c054ef62f4678332fc7c03c4b00a1d2206133e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 29 Oct 2021 17:43:38 +1100 Subject: [PATCH] Objects now sync spatial hierarchy more robustly on export, and clean the collection tree in case the user messed it up. --- .../blenderbim/bim/data/ui/presets.py | 453 ++++++++---------- src/blenderbim/blenderbim/bim/export_ifc.py | 39 +- .../blenderbim/bim/module/project/prop.py | 2 +- src/blenderbim/blenderbim/bim/operator.py | 2 +- src/blenderbim/blenderbim/bim/prop.py | 11 +- src/blenderbim/blenderbim/tool/collector.py | 39 ++ .../test/bim/feature/project.feature | 19 +- src/blenderbim/test/bim/test_feature.py | 2 +- src/blenderbim/test/tool/test_collector.py | 22 + 9 files changed, 301 insertions(+), 288 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/data/ui/presets.py b/src/blenderbim/blenderbim/bim/data/ui/presets.py index 90f9d874df..c1866f9d89 100644 --- a/src/blenderbim/blenderbim/bim/data/ui/presets.py +++ b/src/blenderbim/blenderbim/bim/data/ui/presets.py @@ -1,241 +1,214 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# This file is part of BlenderBIM Add-on. +# +# BlenderBIM Add-on is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BlenderBIM Add-on is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BlenderBIM Add-on. If not, see . + presets = { - "Basic": [ - "project", - "search", - "bcf", - "attribute", - "spatial", - "pset", - "qto" - ], - "Admin": [ - "project", - "search", - "bcf", - "root", - "unit", - "model", - "georeference", - "context", - "drawing", - "misc", - "attribute", - "type", - "spatial", - "void", - "aggregate", - "geometry", - "cobie", - "resource", - "cost", - "sequence", - "group", - "system", - "structural", - "boundary", - "profile", - "material", - "style", - "layer", - "owner", - "pset", - "qto", - "classification", - "constraint", - "document", - "pset_template", - "clash", - "lca", - "csv", - "bimtester", - "diff", - "patch", - "covetool", - "augin", - "debug" - ], - "BIM Coordination": [ - "project", - "search", - "bcf", - "root", - "unit", - "georeference", - "attribute", - "type", - "spatial", - "void", - "aggregate", - "owner", - "pset", - "qto", - "clash", - "csv", - "bimtester", - "diff", - "patch", - "debug" - ], - "Architecture": [ - "project", - "search", - "bcf", - "root", - "unit", - "model", - "georeference", - "context", - "drawing", - "misc", - "attribute", - "type", - "spatial", - "void", - "aggregate", - "geometry", - "group", - "system", - "boundary", - "profile", - "material", - "style", - "layer", - "owner", - "pset", - "qto", - "classification", - "pset_template", - "clash", - "csv" - ], - "Structural Engineering": [ - "project", - "search", - "bcf", - "root", - "unit", - "model", - "georeference", - "context", - "drawing", - "misc", - "attribute", - "type", - "spatial", - "void", - "aggregate", - "geometry", - "group", - "system", - "structural", - "boundary", - "profile", - "material", - "style", - "layer", - "owner", - "pset", - "qto", - "classification", - "pset_template", - "clash", - "csv" - ], - "MEP": [ - "project", - "search", - "bcf", - "root", - "unit", - "model", - "georeference", - "context", - "drawing", - "misc", - "attribute", - "type", - "spatial", - "void", - "aggregate", - "geometry", - "group", - "system", - "boundary", - "profile", - "material", - "style", - "layer", - "owner", - "pset", - "qto", - "classification", - "pset_template", - "clash", - "csv" - ], - "Quantity Surveying": [ - "project", - "search", - "attribute", - "spatial", - "void", - "pset", - "qto", - "classification", - "csv" - ], - "Model Enrichment": [ - "project", - "search", - "root", - "attribute", - "spatial", - "pset", - "qto", - "pset_template", - "csv", - "patch" - ], - "3D Visualisation": [ - "project", - "search", - "bcf", - "attribute", - "spatial", - "pset", - "qto" - ], - "4D Scheduling": [ - "project", - "search", - "attribute", - "spatial", - "resource", - "cost", - "sequence", - "pset", - "qto" - ], - "5D Cost Management": [ - "project", - "search", - "attribute", - "spatial", - "resource", - "cost", - "sequence", - "pset", - "qto", - "lca" - ], - "Facility Management": [ - "project", - "search", - "attribute", - "spatial", - "cobie", - "pset", - "qto", - "lca" - ] -} \ No newline at end of file + "Basic": ["project", "search", "bcf", "attribute", "spatial", "pset", "qto"], + "Admin": [ + "project", + "search", + "bcf", + "root", + "unit", + "model", + "georeference", + "context", + "drawing", + "misc", + "attribute", + "type", + "spatial", + "void", + "aggregate", + "geometry", + "cobie", + "resource", + "cost", + "sequence", + "group", + "system", + "structural", + "boundary", + "profile", + "material", + "style", + "layer", + "owner", + "pset", + "qto", + "classification", + "constraint", + "document", + "pset_template", + "clash", + "lca", + "csv", + "bimtester", + "diff", + "patch", + "covetool", + "augin", + "debug", + ], + "BIM Coordination": [ + "project", + "search", + "bcf", + "root", + "unit", + "georeference", + "attribute", + "type", + "spatial", + "void", + "aggregate", + "owner", + "pset", + "qto", + "clash", + "csv", + "bimtester", + "diff", + "patch", + "debug", + ], + "Architecture": [ + "project", + "search", + "bcf", + "root", + "unit", + "model", + "georeference", + "context", + "drawing", + "misc", + "attribute", + "type", + "spatial", + "void", + "aggregate", + "geometry", + "group", + "system", + "boundary", + "profile", + "material", + "style", + "layer", + "owner", + "pset", + "qto", + "classification", + "pset_template", + "clash", + "csv", + ], + "Structural Engineering": [ + "project", + "search", + "bcf", + "root", + "unit", + "model", + "georeference", + "context", + "drawing", + "misc", + "attribute", + "type", + "spatial", + "void", + "aggregate", + "geometry", + "group", + "system", + "structural", + "boundary", + "profile", + "material", + "style", + "layer", + "owner", + "pset", + "qto", + "classification", + "pset_template", + "clash", + "csv", + ], + "MEP": [ + "project", + "search", + "bcf", + "root", + "unit", + "model", + "georeference", + "context", + "drawing", + "misc", + "attribute", + "type", + "spatial", + "void", + "aggregate", + "geometry", + "group", + "system", + "boundary", + "profile", + "material", + "style", + "layer", + "owner", + "pset", + "qto", + "classification", + "pset_template", + "clash", + "csv", + ], + "Quantity Surveying": ["project", "search", "attribute", "spatial", "void", "pset", "qto", "classification", "csv"], + "Model Enrichment": [ + "project", + "search", + "root", + "attribute", + "spatial", + "pset", + "qto", + "pset_template", + "csv", + "patch", + ], + "3D Visualisation": ["project", "search", "bcf", "attribute", "spatial", "pset", "qto"], + "4D Scheduling": ["project", "search", "attribute", "spatial", "resource", "cost", "sequence", "pset", "qto"], + "5D Cost Management": [ + "project", + "search", + "attribute", + "spatial", + "resource", + "cost", + "sequence", + "pset", + "qto", + "lca", + ], + "Facility Management": ["project", "search", "attribute", "spatial", "cobie", "pset", "qto", "lca"], +} diff --git a/src/blenderbim/blenderbim/bim/export_ifc.py b/src/blenderbim/blenderbim/bim/export_ifc.py index 6a2d345bb4..100e5bf830 100644 --- a/src/blenderbim/blenderbim/bim/export_ifc.py +++ b/src/blenderbim/blenderbim/bim/export_ifc.py @@ -91,8 +91,8 @@ class IfcExporter: try: if isinstance(obj, bpy.types.Material): continue + tool.Collector.sync(obj) self.sync_object_placement(obj) - self.sync_object_container(ifc_definition_id, obj) except ReferenceError: pass # The object is likely deleted if self.should_delete(obj): @@ -149,43 +149,6 @@ class IfcExporter: if grid_obj.matrix_world != obj.matrix_world: bpy.ops.bim.update_representation(obj=obj.name) - def sync_object_container(self, guid, obj): - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - element_collection = bpy.data.collections.get(obj.name) - - if self.file.schema == "IFC2X3": - if element.is_a("IfcProject"): - return - elif element.is_a("IfcContext"): - return - - if ( - (element.is_a("IfcElement") and element_collection) - or element.is_a("IfcSpatialStructureElement") - or element.is_a("IfcGrid") - ): - try: - parent_collection = [c for c in bpy.data.collections if c.children.get(element_collection.name)][0] - except: - return # Out of the spatial tree - else: - parent_collection = obj.users_collection[0] - - parent_obj = bpy.data.objects.get(parent_collection.name) - if not parent_obj or not parent_obj.BIMObjectProperties.ifc_definition_id: - return - parent = self.file.by_id(parent_obj.BIMObjectProperties.ifc_definition_id) - - if parent.is_a("IfcSpatialStructureElement") and not element.is_a("IfcSpatialStructureElement"): - if parent != ifcopenshell.util.element.get_container(element): - blenderbim.core.spatial.assign_container( - tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj - ) - elif parent != ifcopenshell.util.element.get_aggregate(element): - blenderbim.core.aggregate.assign_object( - tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj - ) - def should_delete(self, obj): try: # This will throw an exception if the Blender object no longer exists diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index 825194d253..f5343d89fe 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -111,7 +111,7 @@ class BIMProjectProperties(PropertyGroup): filter_categories: CollectionProperty(name="Filter Categories", type=FilterCategory) active_filter_category_index: IntProperty(name="Active Filter Category Index") filter_query: StringProperty(name="Filter Query") - should_filter_spatial_elements: BoolProperty(name="Filter Spatial Elements", default=True) + should_filter_spatial_elements: BoolProperty(name="Filter Spatial Elements", default=False) should_use_cpu_multiprocessing: BoolProperty(name="Import with CPU Multiprocessing", default=True) should_merge_by_class: BoolProperty(name="Import and Merge by Class", default=False) should_merge_by_material: BoolProperty(name="Import and Merge by Material", default=False) diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 44eda63619..864fe0bb40 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -442,7 +442,7 @@ class ConfigureVisibility(bpy.types.Operator): def draw(self, context): layout = self.layout - + layout.prop(context.scene.BIMProperties, "ui_preset") layout.separator() layout.label(text="Adjust the modules to your liking:") diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index 1bd1401b50..cc12751c03 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -43,7 +43,7 @@ materialpsetnames_enum = [] def update_preset(self, context): from blenderbim.bim.data.ui.presets import presets - + module_visibility = context.scene.BIMProperties.module_visibility chosen_preset = context.scene.BIMProperties.ui_preset @@ -52,9 +52,10 @@ def update_preset(self, context): def load_presets(self, context): - from blenderbim.bim.data.ui.presets import presets - return [(preset,preset,"") for preset in presets.keys()] - + from blenderbim.bim.data.ui.presets import presets + + return [(preset, preset, "") for preset in presets.keys()] + def update_is_visible(self, context): from blenderbim.bim import modules @@ -222,7 +223,7 @@ class BIMProperties(PropertyGroup): name="UI Preset", description="Select from one of the available UI presets, or configure the modules to your preference below", update=update_preset, - items=load_presets + items=load_presets, ) module_visibility: CollectionProperty(name="Module Visibility", type=ModuleVisibility) schema_dir: StringProperty( diff --git a/src/blenderbim/blenderbim/tool/collector.py b/src/blenderbim/blenderbim/tool/collector.py index 0b8b9eb7e1..3a122fee25 100644 --- a/src/blenderbim/blenderbim/tool/collector.py +++ b/src/blenderbim/blenderbim/tool/collector.py @@ -24,6 +24,45 @@ import blenderbim.tool as tool class Collector(blenderbim.core.tool.Collector): + @classmethod + def sync(cls, obj): + # This is the reverse of assign. It reads the Blender collection and figures out its IFC hierarchy + element = tool.Ifc.get_entity(obj) + + if element.is_a("IfcProject") or element.is_a("IfcGridAxis"): + return + + object_collection = None + collection_collection = None + + 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) + + parent_collection = None + + if obj.users_collection != (object_collection,) and obj.users_collection[0].name != obj.name: + parent_collection = obj.users_collection[0] + elif collection_collection and collection_collection.children.find(object_collection.name) == -1: + for collection in bpy.data.collections: + if collection.children.find(obj.users_collection[0].name) != -1: + parent_collection = collection + break + + if parent_collection: + parent_obj = bpy.data.objects.get(parent_collection.name) + parent = tool.Ifc.get_entity(parent_obj) + 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 + ) + blenderbim.core.aggregate.assign_object( + tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj + ) + @classmethod def assign(cls, obj): element = tool.Ifc.get_entity(obj) diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature index f3327f77f2..237a73ed67 100644 --- a/src/blenderbim/test/bim/feature/project.feature +++ b/src/blenderbim/test/bim/feature/project.feature @@ -104,6 +104,7 @@ Scenario: Load project elements - load objects filtered by decomposition And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc', is_advanced=True)" When I set "scene.BIMProjectProperties.collection_mode" to "DECOMPOSITION" And I set "scene.BIMProjectProperties.filter_mode" to "DECOMPOSITION" + And I set "scene.BIMProjectProperties.should_filter_spatial_elements" to "True" Then "scene.BIMProjectProperties.filter_categories['IfcSite/My Site'].total_elements" is "0" Then "scene.BIMProjectProperties.filter_categories['IfcBuilding/My Building'].total_elements" is "0" Then "scene.BIMProjectProperties.filter_categories['IfcBuildingStorey/Ground Floor'].total_elements" is "1" @@ -131,6 +132,7 @@ Scenario: Load project elements - load objects filtered by IFC class And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc', is_advanced=True)" When I set "scene.BIMProjectProperties.collection_mode" to "DECOMPOSITION" And I set "scene.BIMProjectProperties.filter_mode" to "IFC_CLASS" + And I set "scene.BIMProjectProperties.should_filter_spatial_elements" to "True" Then "scene.BIMProjectProperties.filter_categories['IfcWall'].total_elements" is "1" And "scene.BIMProjectProperties.filter_categories['IfcSlab'].total_elements" is "1" And "scene.BIMProjectProperties.filter_categories['IfcElementAssembly'].total_elements" is "1" @@ -155,6 +157,7 @@ Scenario: Load project elements - load objects filtered by whitelist When I set "scene.BIMProjectProperties.collection_mode" to "DECOMPOSITION" And I set "scene.BIMProjectProperties.filter_mode" to "WHITELIST" And I set "scene.BIMProjectProperties.filter_query" to ".IfcSlab" + And I set "scene.BIMProjectProperties.should_filter_spatial_elements" to "True" And I press "bim.load_project_elements" Then the object "IfcProject/My Project" is an "IfcProject" And the object "IfcSite/My Site" is an "IfcSite" @@ -174,6 +177,7 @@ Scenario: Load project elements - load objects filtered by blacklist When I set "scene.BIMProjectProperties.collection_mode" to "DECOMPOSITION" And I set "scene.BIMProjectProperties.filter_mode" to "BLACKLIST" And I set "scene.BIMProjectProperties.filter_query" to ".IfcSlab" + And I set "scene.BIMProjectProperties.should_filter_spatial_elements" to "True" And I press "bim.load_project_elements" Then the object "IfcProject/My Project" is an "IfcProject" And the object "IfcSite/My Site" is an "IfcSite" @@ -196,6 +200,7 @@ Scenario: Load project elements - load no objects due to filter And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc', is_advanced=True)" When I set "scene.BIMProjectProperties.collection_mode" to "DECOMPOSITION" And I set "scene.BIMProjectProperties.filter_mode" to "IFC_CLASS" + And I set "scene.BIMProjectProperties.should_filter_spatial_elements" to "True" And I press "bim.load_project_elements" Then the object "IfcProject/My Project" is an "IfcProject" And the object "IfcSite/My Site" does not exist @@ -325,7 +330,7 @@ Scenario: Export IFC - with moved object location synchronised Given an empty IFC project When the object "IfcBuildingStorey/My Storey" is moved to "0,0,1" And I press "export_ifc.bim(filepath='{cwd}/test/files/export.ifc')" - And an empty Blender session + And an empty Blender session is started And I press "bim.load_project(filepath='{cwd}/test/files/export.ifc')" Then the object "IfcBuildingStorey/My Storey" is at "0,0,1" @@ -334,6 +339,16 @@ Scenario: Export IFC - with moved grid axis location synchronised And I press "mesh.add_grid" When the object "IfcGridAxis/01" is moved to "1,0,0" And I press "export_ifc.bim(filepath='{cwd}/test/files/export.ifc')" - And an empty Blender session + And an empty Blender session is started And I press "bim.load_project(filepath='{cwd}/test/files/export.ifc')" Then the object "IfcGridAxis/01" bottom left corner is at "1,-2,0" + +Scenario: Export IFC - with changed spatial container synchronised + Given an empty Blender session + And I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')" + Then the object "IfcSlab/Slab" is in the collection "IfcBuildingStorey/Ground Floor" + When the object "IfcSlab/Slab" is placed in the collection "IfcBuildingStorey/Level 1" + And I press "export_ifc.bim(filepath='{cwd}/test/files/export.ifc')" + And an empty Blender session is started + And I press "bim.load_project(filepath='{cwd}/test/files/export.ifc')" + Then the object "IfcSlab/Slab" is in the collection "IfcBuildingStorey/Level 1" diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index d42ac7d34e..decf07405e 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -41,7 +41,7 @@ def replace_variables(value): @given("an empty Blender session") -@when("an empty Blender session") +@when("an empty Blender session is started") def an_empty_ifc_project(): IfcStore.purge() bpy.ops.wm.read_homefile(app_template="") diff --git a/src/blenderbim/test/tool/test_collector.py b/src/blenderbim/test/tool/test_collector.py index 920c24bc01..390c07da7c 100644 --- a/src/blenderbim/test/tool/test_collector.py +++ b/src/blenderbim/test/tool/test_collector.py @@ -214,3 +214,25 @@ class TestAssign(NewFile): subject.assign(axis_obj) assert axis_obj.users_collection[0].name == "UAxes" assert bpy.data.collections.get("IfcGrid/Name").children.get("UAxes") + + +class TestSync(NewFile): + def test_in_decomposition_mode_elements_can_be_in_spatial_containers(self): + bpy.ops.bim.create_project() + wall_obj = bpy.data.objects.new("Object", None) + wall_element = tool.Ifc.get().createIfcWall() + tool.Ifc.link(wall_element, wall_obj) + bpy.data.collections.get("IfcSite/My Site").objects.link(wall_obj) + subject.sync(wall_obj) + assert ifcopenshell.util.element.get_container(wall_element).is_a("IfcSite") + + def test_in_decomposition_mode_elements_can_aggregate(self): + bpy.ops.bim.create_project() + obj = bpy.data.objects.new("IfcBuildingStorey/Name", None) + col = bpy.data.collections.new("IfcBuildingStorey/Name") + element = tool.Ifc.get().createIfcBuildingStorey(Name="Name") + tool.Ifc.link(element, obj) + bpy.data.collections.get("IfcBuilding/My Building").children.link(col) + col.objects.link(obj) + subject.sync(obj) + assert ifcopenshell.util.element.get_aggregate(element).is_a("IfcBuilding")