Objects now sync spatial hierarchy more robustly on export, and clean the collection tree in case the user messed it up.

This commit is contained in:
Dion Moult
2021-10-29 17:43:38 +11:00
parent 1141fd5784
commit a4c054ef62
9 changed files with 301 additions and 288 deletions
+213 -240
View File
@@ -1,241 +1,214 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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"
]
}
"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"],
}
+1 -38
View File
@@ -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
@@ -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)
+1 -1
View File
@@ -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:")
+6 -5
View File
@@ -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(
@@ -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)
@@ -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"
+1 -1
View File
@@ -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="")
@@ -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")