diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index da786c33a2..8dbf5aace3 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -21,6 +21,7 @@ import bpy import time import bmesh import shutil +import logging import threading import mathutils import numpy as np @@ -1610,7 +1611,8 @@ class IfcImporter: if "-" in geometry.id: mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.split("-")[0]) else: - mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id) + # TODO: See #2002 + mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.replace(",", "")) def set_matrix_world(self, obj, matrix_world): obj.matrix_world = matrix_world @@ -1637,11 +1639,13 @@ class IfcImportSettings: self.collection_mode = "DECOMPOSITION" @staticmethod - def factory(context, input_file, logger): - scene_diff = context.scene.DiffProperties - props = context.scene.BIMProjectProperties + def factory(context=None, input_file=None, logger=None): + scene_diff = bpy.context.scene.DiffProperties + props = bpy.context.scene.BIMProjectProperties settings = IfcImportSettings() settings.input_file = input_file + if logger is None: + logger = logging.getLogger("ImportIFC") settings.logger = logger settings.diff_file = scene_diff.diff_json_file settings.collection_mode = props.collection_mode diff --git a/src/blenderbim/blenderbim/bim/module/attribute/ui.py b/src/blenderbim/blenderbim/bim/module/attribute/ui.py index cc533a1183..47ff4cac76 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/ui.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/ui.py @@ -81,8 +81,6 @@ class BIM_PT_material_attributes(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" - bl_parent_id = "BIM_PT_object_metadata" - bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/demo/prop.py b/src/blenderbim/blenderbim/bim/module/demo/prop.py index d603181b09..570eaeed02 100644 --- a/src/blenderbim/blenderbim/bim/module/demo/prop.py +++ b/src/blenderbim/blenderbim/bim/module/demo/prop.py @@ -32,6 +32,7 @@ import bpy from bpy.types import PropertyGroup + # Properties have many different data types. We won't use all of them in this # demo module, but this is a list for your reference. from bpy.props import ( diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 378794653f..1cbe8eb3df 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -53,5 +53,8 @@ def update_ifc_class(self, context): class BIMModelProperties(PropertyGroup): ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="IFC Class", update=update_ifc_class) relating_type: bpy.props.EnumProperty(items=get_relating_type, name="Relating Type") - occurrence_name_style: bpy.props.EnumProperty(items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")], name="Occurrence Name Style") + occurrence_name_style: bpy.props.EnumProperty( + items=[("CLASS", "By Class", ""), ("TYPE", "By Type", ""), ("CUSTOM", "Custom", "")], + name="Occurrence Name Style", + ) occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function") diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py index 7610039899..a5a6ee35a2 100644 --- a/src/blenderbim/blenderbim/bim/module/system/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py @@ -26,14 +26,17 @@ classes = ( operator.DisableSystemEditingUI, operator.EditSystem, operator.EnableEditingSystem, + operator.HidePorts, operator.LoadSystems, operator.RemoveSystem, operator.SelectSystemProducts, + operator.ShowPorts, operator.UnassignSystem, prop.System, prop.BIMSystemProperties, ui.BIM_PT_systems, ui.BIM_PT_object_systems, + ui.BIM_PT_ports, ui.BIM_UL_systems, ui.BIM_UL_object_systems, ) diff --git a/src/blenderbim/blenderbim/bim/module/system/data.py b/src/blenderbim/blenderbim/bim/module/system/data.py index f2af809ec0..856454c8a9 100644 --- a/src/blenderbim/blenderbim/bim/module/system/data.py +++ b/src/blenderbim/blenderbim/bim/module/system/data.py @@ -80,3 +80,20 @@ class ObjectSystemData: @classmethod def total_systems(cls): return len(tool.Ifc.get().by_type("IfcSystem")) + + +class PortData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.data = { + "total_ports": cls.total_ports(), + } + cls.is_loaded = True + + @classmethod + def total_ports(cls): + element = tool.Ifc.get_entity(bpy.context.active_object) + return len(ifcopenshell.util.system.get_ports(element)) diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index aaf3a623b6..ff1668f401 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -132,3 +132,21 @@ class SelectSystemProducts(bpy.types.Operator, Operator): def _execute(self, context): core.select_system_products(tool.System, system=tool.Ifc.get().by_id(self.system)) + + +class ShowPorts(bpy.types.Operator, Operator): + bl_idname = "bim.show_ports" + bl_label = "Show Ports" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.show_ports(tool.System, element=tool.Ifc.get_entity(context.active_object)) + + +class HidePorts(bpy.types.Operator, Operator): + bl_idname = "bim.hide_ports" + bl_label = "Hide Ports" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.hide_ports(tool.System, element=tool.Ifc.get_entity(context.active_object)) diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index b4ce788937..11fa45fba5 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -16,9 +16,10 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import blenderbim.tool as tool from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore -from blenderbim.bim.module.system.data import SystemData, ObjectSystemData +from blenderbim.bim.module.system.data import SystemData, ObjectSystemData, PortData class BIM_PT_systems(Panel): @@ -125,6 +126,35 @@ class BIM_PT_object_systems(Panel): self.layout.label(text="No System associated with Active Object") +class BIM_PT_ports(Panel): + bl_label = "IFC Ports" + bl_idname = "BIM_PT_ports" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_parent_id = "BIM_PT_services_object" + + @classmethod + def poll(cls, context): + if not context.active_object: + return False + element = tool.Ifc.get_entity(context.active_object) + if not element or not element.is_a("IfcDistributionElement"): + return False + return True + + def draw(self, context): + if not PortData.is_loaded: + PortData.load() + self.props = context.scene.BIMSystemProperties + + row = self.layout.row(align=True) + row.label(text=f"{PortData.data['total_ports']} Ports Found", icon="PLUGIN") + row.operator("bim.show_ports", icon="HIDE_OFF", text="") + row.operator("bim.hide_ports", icon="HIDE_ON", text="") + + class BIM_UL_systems(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): system_icons = { diff --git a/src/blenderbim/blenderbim/core/system.py b/src/blenderbim/blenderbim/core/system.py index ae58433f72..10f73c8c63 100644 --- a/src/blenderbim/blenderbim/core/system.py +++ b/src/blenderbim/blenderbim/core/system.py @@ -64,3 +64,13 @@ def unassign_system(ifc, system=None, product=None): def select_system_products(system_tool, system=None): system_tool.select_system_products(system) + + +def show_ports(system, element=None): + ports = system.get_ports(element) + system.load_ports(ports) + system.select_elements(ports) + + +def hide_ports(system, element=None): + system.delete_element_objects(system.get_ports(element)) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index ebadfb8186..9b5d1df1a6 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -359,12 +359,16 @@ class Surveyor: @interface class System: + def delete_element_objects(cls, elements): pass def disable_editing_system(cls): pass def disable_system_editing_ui(cls): pass def enable_system_editing_ui(cls): pass def export_system_attributes(cls): pass + def get_ports(cls, element): pass def import_system_attributes(cls, system): pass def import_systems(cls): pass + def load_ports(cls, port): pass + def select_elements(cls, elements): pass def select_system_products(cls, system): pass def set_active_system(cls, system): pass diff --git a/src/blenderbim/blenderbim/tool/boundary.py b/src/blenderbim/blenderbim/tool/boundary.py index 4e2338b302..d8c8cda521 100644 --- a/src/blenderbim/blenderbim/tool/boundary.py +++ b/src/blenderbim/blenderbim/tool/boundary.py @@ -58,9 +58,7 @@ class Boundary(blenderbim.core.tool.Boundary): def polyline_from_indexes(cls, mesh, indexes): return tuple(mesh.vertices[i].co for i in indexes) - @classmethod def polyline_to_2d(cls, polyline, placement_matrix): matrix_inv = placement_matrix.inverted() return tuple((matrix_inv @ v).to_2d() for v in polyline) - diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index 906510f89b..1c5a27f0ce 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -20,9 +20,17 @@ import bpy import ifcopenshell.util.system import blenderbim.core.tool import blenderbim.tool as tool +from blenderbim.bim import import_ifc class System(blenderbim.core.tool.System): + @classmethod + def delete_element_objects(cls, elements): + for element in elements: + obj = tool.Ifc.get_object(element) + if obj: + bpy.data.objects.remove(obj) + @classmethod def disable_editing_system(cls): bpy.context.scene.BIMSystemProperties.active_system_id = 0 @@ -39,6 +47,10 @@ class System(blenderbim.core.tool.System): def export_system_attributes(cls): return blenderbim.bim.helper.export_attributes(bpy.context.scene.BIMSystemProperties.system_attributes) + @classmethod + def get_ports(cls, element): + return ifcopenshell.util.system.get_ports(element) + @classmethod def import_system_attributes(cls, system): blenderbim.bim.helper.import_attributes2(system, bpy.context.scene.BIMSystemProperties.system_attributes) @@ -56,12 +68,31 @@ class System(blenderbim.core.tool.System): new.ifc_class = system.is_a() @classmethod - def select_system_products(cls, system): - for element in ifcopenshell.util.system.get_system_elements(system): + def load_ports(cls, ports): + if not ports: + return + ifc_import_settings = import_ifc.IfcImportSettings.factory() + ifc_importer = import_ifc.IfcImporter(ifc_import_settings) + ifc_importer.file = tool.Ifc.get() + ifc_importer.calculate_unit_scale() + ports = set(ports) + ports -= ifc_importer.create_products(ports) + if ports: + for port in ports: + ifc_importer.create_product(port) + ifc_importer.place_objects_in_collections() + + @classmethod + def select_elements(cls, elements): + for element in elements: obj = tool.Ifc.get_object(element) if obj: obj.select_set(True) + @classmethod + def select_system_products(cls, system): + cls.select_elements(ifcopenshell.util.system.get_system_elements(system)) + @classmethod def set_active_system(cls, system): bpy.context.scene.BIMSystemProperties.active_system_id = system.id() diff --git a/src/blenderbim/test/core/test_demo.py b/src/blenderbim/test/core/test_demo.py index dca59f1e79..786e6fd3b2 100644 --- a/src/blenderbim/test/core/test_demo.py +++ b/src/blenderbim/test/core/test_demo.py @@ -33,6 +33,7 @@ # We always call the module we're testing the "test subject". This makes our # tests simple to read: just look for where the "subject" is called! import blenderbim.core.demo as subject + # These are like mocks, stubs, or spy objects. They don't do anything, but they # let us check our test expectations. from test.core.bootstrap import ifc, demo diff --git a/src/blenderbim/test/core/test_system.py b/src/blenderbim/test/core/test_system.py index 02fe512cd2..6f47b361e5 100644 --- a/src/blenderbim/test/core/test_system.py +++ b/src/blenderbim/test/core/test_system.py @@ -88,3 +88,18 @@ class TestSelectSystemProducts: def test_run(self, system): system.select_system_products("system").should_be_called() subject.select_system_products(system, system="system") + + +class TestShowPorts: + def test_run(self, system): + system.get_ports("element").should_be_called().will_return(["port"]) + system.load_ports(["port"]).should_be_called() + system.select_elements(["port"]).should_be_called() + subject.show_ports(system, element="element") + + +class TestHidePorts: + def test_run(self, system): + system.get_ports("element").should_be_called().will_return(["port"]) + system.delete_element_objects(["port"]).should_be_called() + subject.hide_ports(system, element="element") diff --git a/src/blenderbim/test/tool/test_brick.py b/src/blenderbim/test/tool/test_brick.py index 855600d27b..7fd59987d4 100644 --- a/src/blenderbim/test/tool/test_brick.py +++ b/src/blenderbim/test/tool/test_brick.py @@ -350,11 +350,7 @@ class TestRemoveBrick(NewFile): BrickStore.graph = brickschema.Graph() result = subject.add_brick("http://example.org/digitaltwin#", "https://brickschema.org/schema/Brick#Equipment") subject.remove_brick(result) - assert not list( - BrickStore.graph.triples( - (URIRef(result), None, None) - ) - ) + assert not list(BrickStore.graph.triples((URIRef(result), None, None))) class TestRunAssignBrickReference(NewFile): diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py index 005aa0e3d5..68c91a142e 100644 --- a/src/blenderbim/test/tool/test_model.py +++ b/src/blenderbim/test/tool/test_model.py @@ -48,5 +48,5 @@ class TestGenerateOccurrenceName(NewFile): ifc = ifcopenshell.file() element_type = ifc.createIfcWallType() bpy.context.scene.BIMModelProperties.occurrence_name_style = "CUSTOM" - bpy.context.scene.BIMModelProperties.occurrence_name_function = "\"Foobar\"" + bpy.context.scene.BIMModelProperties.occurrence_name_function = '"Foobar"' assert subject.generate_occurrence_name(element_type, "IfcWall") == "Foobar" diff --git a/src/blenderbim/test/tool/test_system.py b/src/blenderbim/test/tool/test_system.py index c94a32c76b..c6c698ec2b 100644 --- a/src/blenderbim/test/tool/test_system.py +++ b/src/blenderbim/test/tool/test_system.py @@ -29,6 +29,17 @@ class TestImplementsTool(NewFile): assert isinstance(subject(), blenderbim.core.tool.System) +class TestDeleteElementObjects(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + obj = bpy.data.objects.new("Object", None) + element = ifc.createIfcWall() + tool.Ifc.link(element, obj) + subject.delete_element_objects([element]) + assert not bpy.data.objects.get("Object") + + class TestDisableEditingSystem(NewFile): def test_run(self): bpy.context.scene.BIMSystemProperties.active_system_id = 10 @@ -60,6 +71,16 @@ class TestExportSystemAttributes(NewFile): } +class TestGetPorts(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + element = ifc.createIfcDuctSegment() + port = ifc.createIfcDistributionPort() + ifcopenshell.api.run("system.assign_port", ifc, element=element, port=port) + subject.get_ports(element) == [port] + + class TestImportSystemAttributes(NewFile): def test_importing_a_system(self): ifc = ifcopenshell.file() @@ -129,6 +150,32 @@ class TestImportSystems(NewFile): assert props.systems[0].ifc_class == "IfcDistributionSystem" +class TestLoadPorts(NewFile): + def test_run(self): + ifc = ifcopenshell.api.run("project.create_file") + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + ifcopenshell.api.run("unit.assign_unit", ifc) + tool.Ifc().set(ifc) + port = ifc.createIfcDistributionPort() + subject.load_ports([port]) + obj = tool.Ifc.get_object(port) + assert obj + assert obj.users_collection + assert list(obj.location) == [0, 0, 0] + + +class TestSelectElements(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc().set(ifc) + element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcPump") + obj = bpy.data.objects.new("Object", None) + bpy.context.scene.collection.objects.link(obj) + tool.Ifc.link(element, obj) + subject.select_elements([element]) + assert obj in bpy.context.selected_objects + + class TestSelectSystemProducts(NewFile): def test_run(self): ifc = ifcopenshell.file()