From a9f76fc8c0c36d9918fdc488fab2ab133c24ffb3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 12 Sep 2021 15:25:03 +1000 Subject: [PATCH] You can now partially load an IFC filtered by spatial containers. --- src/blenderbim/blenderbim/bim/import_ifc.py | 92 +++++++++---------- .../blenderbim/bim/module/project/__init__.py | 2 + .../blenderbim/bim/module/project/operator.py | 30 ++++-- .../blenderbim/bim/module/project/prop.py | 43 ++++++++- .../blenderbim/bim/module/project/ui.py | 27 +++++- src/blenderbim/test/bim/bootstrap.py | 5 + .../test/bim/module/project/test_operator.py | 28 +++++- 7 files changed, 163 insertions(+), 64 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 326a4fea29..6e344744bb 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -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 diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index 31c0a48143..5fb2b055bf 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -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, ) diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 4a8a122f94..a11f106cc4 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -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) diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py index bb5545f6dc..d4e450ae52 100644 --- a/src/blenderbim/blenderbim/bim/module/project/prop.py +++ b/src/blenderbim/blenderbim/bim/module/project/prop.py @@ -17,20 +17,34 @@ # along with BlenderBIM Add-on. If not, see . 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): diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 5764ef085a..5592af5df1 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -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, + ) diff --git a/src/blenderbim/test/bim/bootstrap.py b/src/blenderbim/test/bim/bootstrap.py index facd60a0b1..b385f25ce2 100644 --- a/src/blenderbim/test/bim/bootstrap.py +++ b/src/blenderbim/test/bim/bootstrap.py @@ -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, diff --git a/src/blenderbim/test/bim/module/project/test_operator.py b/src/blenderbim/test/bim/module/project/test_operator.py index db7cdfa226..187e32dbc9 100644 --- a/src/blenderbim/test/bim/module/project/test_operator.py +++ b/src/blenderbim/test/bim/module/project/test_operator.py @@ -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 + """