You can now partially load an IFC filtered by spatial containers.

This commit is contained in:
Dion Moult
2021-09-12 15:25:03 +10:00
parent 48cb598975
commit a9f76fc8c0
7 changed files with 163 additions and 64 deletions
+43 -49
View File
@@ -18,7 +18,6 @@
import re
import bpy
import json
import time
import bmesh
import shutil
@@ -164,12 +163,10 @@ class IfcImporter:
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
self.settings_2d = ifcopenshell.geom.settings()
self.settings_2d.set(self.settings_2d.INCLUDE_CURVES, True)
self.filter_mode = None
self.include_elements = set()
self.exclude_elements = set()
self.iterator_elements = set()
self.project = None
self.spatial_structure_elements = {}
self.elements = []
self.elements = set()
self.type_collection = None
self.type_products = {}
self.openings = {}
@@ -210,10 +207,10 @@ class IfcImporter:
self.profile_code("Set units")
self.create_project()
self.profile_code("Create project")
self.create_spatial_hierarchy()
self.profile_code("Create spatial hierarchy")
self.process_element_filter()
self.profile_code("Process element filter")
self.create_collections()
self.profile_code("Create collections")
self.create_aggregates()
self.profile_code("Create aggregates")
self.create_aggregate_tree()
@@ -278,32 +275,17 @@ class IfcImporter:
return abs(coords[0]) > limit or abs(coords[1]) > limit or abs(coords[2]) > limit
def process_element_filter(self):
if self.ifc_import_settings.ifc_import_filter == "NONE" or not self.ifc_import_settings.ifc_selector:
self.elements = self.file.by_type("IfcElement")
return
selector = ifcopenshell.util.selector.Selector()
elements = selector.parse(self.file, self.ifc_import_settings.ifc_selector)
if self.ifc_import_settings.ifc_import_filter == "WHITELIST":
self.filter_mode = "WHITELIST"
self.include_elements = set(elements)
self.elements = self.include_elements
elif self.ifc_import_settings.ifc_import_filter == "BLACKLIST":
self.filter_mode = "BLACKLIST"
self.exclude_elements = set(elements)
self.elements = [e for e in self.file.by_type("IfcElement") if e not in self.exclude_elements]
if self.ifc_import_settings.has_filter:
self.elements = set(self.ifc_import_settings.elements)
else:
self.elements = set(self.file.by_type("IfcElement"))
self.iterator_elements = set(self.elements)
def parse_native_elements(self):
if self.filter_mode == "WHITELIST":
for element in self.include_elements:
if self.is_native(element):
self.native_elements[element.GlobalId] = element
self.include_elements -= self.native_elements
elif self.filter_mode == "BLACKLIST":
for element in set(self.file.by_type("IfcElement")) - self.exclude_elements:
if self.is_native(element):
self.native_elements.add(element)
self.exclude_elements |= self.native_elements
for element in self.elements:
if self.is_native(element):
self.native_elements.add(element)
self.iterator_elements -= self.native_elements
def is_native(self, element):
if (
@@ -500,7 +482,7 @@ class IfcImporter:
self.type_collection = bpy.data.collections.new("Types")
self.project["blender"].children.link(self.type_collection)
if self.filter_mode in ["WHITELIST", "BLACKLIST"]:
if self.ifc_import_settings.has_filter:
type_products = set([ifcopenshell.util.element.get_type(e) for e in self.elements])
else:
type_products = self.file.by_type("IfcTypeProduct")
@@ -584,19 +566,16 @@ class IfcImporter:
self.settings,
self.file,
multiprocessing.cpu_count(),
include=self.include_elements or None,
exclude=self.exclude_elements or None,
include=self.iterator_elements or None,
)
else:
iterator = ifcopenshell.geom.iterator(
self.settings, self.file, include=self.include_elements or None, exclude=self.exclude_elements or None
)
iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=self.iterator_elements or None)
valid_file = iterator.initialize()
if not valid_file:
return False
checkpoint = time.time()
total_created = 0
approx_total_products = len(self.include_elements) or len(self.file.by_type("IfcElement"))
approx_total_products = len(self.iterator_elements) or len(self.file.by_type("IfcElement"))
start_progress = self.progress
progress_range = 85 - start_progress
while True:
@@ -988,10 +967,7 @@ class IfcImporter:
)
def create_project(self):
if self.file.schema == "IFC2X3":
self.project = {"ifc": self.file.by_type("IfcProject")[0]}
else:
self.project = {"ifc": self.file.by_type("IfcContext")[0]}
self.project = {"ifc": self.file.by_type("IfcProject")[0]}
self.project["blender"] = bpy.data.collections.new(
"{}/{}".format(self.project["ifc"].is_a(), self.project["ifc"].Name)
)
@@ -999,13 +975,31 @@ class IfcImporter:
if obj:
self.project["blender"].objects.link(obj)
def create_spatial_hierarchy(self):
if self.project["ifc"].IsDecomposedBy:
for rel_aggregate in self.project["ifc"].IsDecomposedBy:
self.add_related_objects(self.project["blender"], rel_aggregate.RelatedObjects)
def create_collections(self):
if self.ifc_import_settings.collection_mode == "DECOMPOSITION" and len(self.file.by_type("IfcRelAggregates")) > 10000:
# More than 10,000 collections makes Blender unhappy
print("Falling back to SPATIAL_DECOMPOSITION collection mode")
self.ifc_import_settings.collection_mode = "SPATIAL_DECOMPOSITION"
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
self.create_decomposition_collections()
def create_decomposition_collections(self):
containers = set([ifcopenshell.util.element.get_container(e) for e in self.elements])
self.decomposition_containers = set()
for container in containers:
while container:
self.decomposition_containers.add(container)
container = ifcopenshell.util.element.get_aggregate(container)
if container and container.is_a("IfcContext"):
container = None
for rel_aggregate in self.project["ifc"].IsDecomposedBy or []:
self.add_related_objects(self.project["blender"], rel_aggregate.RelatedObjects)
def add_related_objects(self, parent, related_objects):
for element in related_objects:
if element not in self.decomposition_containers:
continue
global_id = element.GlobalId
collection = bpy.data.collections.new(self.get_name(element))
self.spatial_structure_elements[global_id] = {"blender": collection}
@@ -1019,7 +1013,7 @@ class IfcImporter:
self.add_related_objects(collection, rel_aggregate.RelatedObjects)
def create_aggregates(self):
if self.filter_mode in ["WHITELIST", "BLACKLIST"]:
if self.ifc_import_settings.has_filter:
rel_aggregates = [e.IsDecomposedBy[0].RelatingObject for e in self.elements if e.IsDecomposedBy]
else:
rel_aggregates = [a for a in self.file.by_type("IfcRelAggregates") if a.RelatingObject.is_a("IfcElement")]
@@ -1448,8 +1442,8 @@ class IfcImportSettings:
self.angular_tolerance = 0.5
self.should_offset_model = False
self.model_offset_coordinates = (0, 0, 0)
self.ifc_import_filter = "NONE"
self.ifc_selector = ""
self.has_filter = None
self.elements = ""
self.collection_mode = "DECOMPOSITION"
@staticmethod
@@ -35,10 +35,12 @@ classes = (
operator.DisableEditingHeader,
operator.EditHeader,
prop.LibraryElement,
prop.FilterCategory,
prop.BIMProjectProperties,
ui.BIM_PT_project,
ui.BIM_PT_project_library,
ui.BIM_UL_library,
ui.BIM_UL_filter_categories,
)
@@ -528,16 +528,10 @@ class LoadProjectElements(bpy.types.Operator):
bl_idname = "bim.load_project_elements"
bl_label = "Load Project Elements"
bl_options = {"REGISTER", "UNDO"}
mode: bpy.props.EnumProperty(
items=[
("ALL", "All", ""),
("WHITELIST", "Whitelist", ""),
("BLACKLIST", "Blacklist", ""),
],
name="Mode",
)
def execute(self, context):
self.props = context.scene.BIMProjectProperties
self.file = IfcStore.get_file()
start = time.time()
logger = logging.getLogger("ImportIFC")
path_log = os.path.join(context.scene.BIMProperties.data_dir, "process.log")
@@ -549,6 +543,9 @@ class LoadProjectElements(bpy.types.Operator):
level=logging.DEBUG,
)
settings = import_ifc.IfcImportSettings.factory(context, context.scene.BIMProperties.ifc_file, logger)
settings.has_filter = self.props.filter_mode != "NONE"
if self.props.filter_mode == "DECOMPOSITION":
settings.elements = self.get_decomposition_elements()
settings.logger.info("Starting import")
ifc_importer = import_ifc.IfcImporter(settings)
ifc_importer.execute()
@@ -556,3 +553,20 @@ class LoadProjectElements(bpy.types.Operator):
print("Import finished in {:.2f} seconds".format(time.time() - start))
context.scene.BIMProjectProperties.is_loading = False
return {"FINISHED"}
def get_decomposition_elements(self):
containers = set()
for filter_category in self.props.filter_categories:
if not filter_category.is_selected:
continue
container = self.file.by_id(filter_category.ifc_definition_id)
while container:
containers.add(container)
container = ifcopenshell.util.element.get_aggregate(container)
if container.is_a("IfcContext"):
container = None
elements = set()
for container in containers:
for rel in container.ContainsElements:
elements.update(rel.RelatedElements)
return list(elements)
@@ -17,20 +17,34 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
def update_filter_mode(self, context):
self.filter_categories.clear()
if self.filter_mode == "NONE":
return
file = IfcStore.get_file()
if self.filter_mode == "DECOMPOSITION":
if file.schema == "IFC2X3":
elements = file.by_type("IfcSpatialStructureElement")
else:
elements = file.by_type("IfcSpatialElement")
for element in elements:
new = self.filter_categories.add()
new.name = "{}/{}".format(element.is_a(), element.Name or "Unnamed")
new.ifc_definition_id = element.id()
new.total_elements = sum([len(r.RelatedElements) for r in element.ContainsElements])
class LibraryElement(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
@@ -38,6 +52,13 @@ class LibraryElement(PropertyGroup):
is_appended: BoolProperty(name="Is Appended", default=False)
class FilterCategory(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
is_selected: BoolProperty(name="Is Selected", default=False)
total_elements: IntProperty(name="Total Elements")
class BIMProjectProperties(PropertyGroup):
is_authoring: BoolProperty(name="Enable Authoring Mode", default=True)
is_editing: BoolProperty(name="Is Editing", default=False)
@@ -56,10 +77,22 @@ class BIMProjectProperties(PropertyGroup):
items=[
("DECOMPOSITION", "Decomposition", "Collections represent aggregates and spatial containers"),
("SPATIAL_DECOMPOSITION", "Spatial Decomposition", "Collections represent spatial containers"),
("IFC_CLASS", "IFC Class", "Collections represention IFC class"),
("IFC_CLASS", "IFC Class", "Collections represent IFC class"),
("NONE", "None", "No collections are created"),
],
name="Collection Mode",
)
filter_mode: bpy.props.EnumProperty(
items=[
("NONE", "None", "No filtering is performed"),
("DECOMPOSITION", "Decomposition", "Filter objects by decomposition"),
("IFC_CLASS", "IFC Class", "Filter objects by class"),
],
name="Filter Mode",
update=update_filter_mode
)
filter_categories: CollectionProperty(name="Filter Categories", type=FilterCategory)
active_filter_category_index: IntProperty(name="Active Filter Category Index")
def get_library_element_index(self, lib_element):
@@ -46,7 +46,18 @@ class BIM_PT_project(Panel):
row = self.layout.row()
row.prop(pprops, "collection_mode")
row = self.layout.row()
row.operator("bim.load_project_elements").mode = "ALL"
row.prop(pprops, "filter_mode")
if pprops.filter_mode == "DECOMPOSITION":
self.layout.template_list(
"BIM_UL_filter_categories",
"",
pprops,
"filter_categories",
pprops,
"active_filter_category_index",
)
row = self.layout.row()
row.operator("bim.load_project_elements")
def draw_project_ui(self, context):
props = context.scene.BIMProperties
@@ -191,3 +202,17 @@ class BIM_UL_library(UIList):
op = row.operator("bim.append_library_element", text="", icon="APPEND_BLEND")
op.definition = item.ifc_definition_id
op.prop_index = data.get_library_element_index(item)
class BIM_UL_filter_categories(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
row.label(text=f"{item.name} ({item.total_elements})")
row.prop(
item,
"is_selected",
icon="CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT",
text="",
emboss=False,
)
+5
View File
@@ -129,6 +129,10 @@ def an_ifc_file_exists():
return ifc
def the_object_name_does_not_exist(name):
assert bpy.data.objects.get(name) is None, "Object exists"
def the_object_name_is_an_ifc_class(name, ifc_class):
ifc = an_ifc_file_exists()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
@@ -245,6 +249,7 @@ definitions = {
'"(.*)" is "(.*)"': prop_is_value,
'I enable "(.*)"': i_enable_prop,
'I press "(.*)"': i_press_operator,
'the object "(.*)" does not exist': the_object_name_does_not_exist,
'the object "(.*)" is an "(.*)"': the_object_name_is_an_ifc_class,
'the object "(.*)" is not an IFC element': the_object_name_is_not_an_ifc_element,
'the object "(.*)" is in the collection "(.*)"': the_object_name_is_in_the_collection_collection,
@@ -52,7 +52,8 @@ class TestLoadProjectElements(test.bim.bootstrap.NewFile):
return """
Given I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I set "scene.BIMProjectProperties.collection_mode" to "DECOMPOSITION"
And I press "bim.load_project_elements(mode='ALL')"
And I set "scene.BIMProjectProperties.filter_mode" to "NONE"
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"
And the object "IfcBuilding/My Building" is an "IfcBuilding"
@@ -68,3 +69,28 @@ class TestLoadProjectElements(test.bim.bootstrap.NewFile):
And the object "IfcWall/Wall" is in the collection "IfcBuildingStorey/Level 1"
And "scene.BIMProjectProperties.is_loading" is "False"
"""
@test.bim.bootstrap.scenario
def test_loading_objects_filtered_by_decomposition(self):
return """
Given I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc')"
When I set "scene.BIMProjectProperties.collection_mode" to "DECOMPOSITION"
And I set "scene.BIMProjectProperties.filter_mode" to "DECOMPOSITION"
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"
Then "scene.BIMProjectProperties.filter_categories['IfcBuildingStorey/Level 1'].total_elements" is "1"
When I set "scene.BIMProjectProperties.filter_categories['IfcBuildingStorey/Ground Floor'].is_selected" 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"
And the object "IfcBuilding/My Building" is an "IfcBuilding"
And the object "IfcBuildingStorey/Ground Floor" is an "IfcBuildingStorey"
And the object "IfcSlab/Slab" is an "IfcSlab"
And the object "IfcSite/My Site" is in the collection "IfcSite/My Site"
And the object "IfcBuilding/My Building" is in the collection "IfcBuilding/My Building"
And the object "IfcBuildingStorey/Ground Floor" is in the collection "IfcBuildingStorey/Ground Floor"
And the object "IfcSlab/Slab" is in the collection "IfcBuildingStorey/Ground Floor"
And the object "IfcBuildingStorey/Level 1" does not exist
And the object "IfcWall/Wall" does not exist
"""