mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
You can now partially load an IFC filtered by spatial containers.
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user