From 10138e28c9bede6e8817278273a76b2394e8c610 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 1 Feb 2022 19:16:13 +1100 Subject: [PATCH] You can now add ports to distribution elements in Blender. --- .../blenderbim/bim/module/system/__init__.py | 1 + .../blenderbim/bim/module/system/data.py | 1 + .../blenderbim/bim/module/system/operator.py | 9 +++ .../blenderbim/bim/module/system/ui.py | 1 + src/blenderbim/blenderbim/core/system.py | 7 ++ src/blenderbim/blenderbim/core/tool.py | 2 + src/blenderbim/blenderbim/tool/drawing.py | 10 ++- src/blenderbim/blenderbim/tool/system.py | 38 ++++++++++- src/blenderbim/test/core/test_system.py | 10 +++ src/blenderbim/test/tool/test_system.py | 17 +++++ .../ifcopenshell/api/system/assign_port.py | 33 +++++++--- .../test/api/system/test_assign_port.py | 65 +++++++++++++++++++ 12 files changed, 182 insertions(+), 12 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/system/__init__.py b/src/blenderbim/blenderbim/bim/module/system/__init__.py index a5a6ee35a2..5a0a122d90 100644 --- a/src/blenderbim/blenderbim/bim/module/system/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/system/__init__.py @@ -20,6 +20,7 @@ import bpy from . import ui, prop, operator classes = ( + operator.AddPort, operator.AddSystem, operator.AssignSystem, operator.DisableEditingSystem, diff --git a/src/blenderbim/blenderbim/bim/module/system/data.py b/src/blenderbim/blenderbim/bim/module/system/data.py index 856454c8a9..c9c00dbf9d 100644 --- a/src/blenderbim/blenderbim/bim/module/system/data.py +++ b/src/blenderbim/blenderbim/bim/module/system/data.py @@ -25,6 +25,7 @@ import blenderbim.tool as tool def refresh(): SystemData.is_loaded = False ObjectSystemData.is_loaded = False + PortData.is_loaded = False class SystemData: diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index ff1668f401..c7337de87d 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -150,3 +150,12 @@ class HidePorts(bpy.types.Operator, Operator): def _execute(self, context): core.hide_ports(tool.System, element=tool.Ifc.get_entity(context.active_object)) + + +class AddPort(bpy.types.Operator, Operator): + bl_idname = "bim.add_port" + bl_label = "Add Ports" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + core.add_port(tool.Ifc, 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 11fa45fba5..ca3b2d383f 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -153,6 +153,7 @@ class BIM_PT_ports(Panel): 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="") + row.operator("bim.add_port", icon="ADD", text="") class BIM_UL_systems(UIList): diff --git a/src/blenderbim/blenderbim/core/system.py b/src/blenderbim/blenderbim/core/system.py index 10f73c8c63..244e212bd3 100644 --- a/src/blenderbim/blenderbim/core/system.py +++ b/src/blenderbim/blenderbim/core/system.py @@ -74,3 +74,10 @@ def show_ports(system, element=None): def hide_ports(system, element=None): system.delete_element_objects(system.get_ports(element)) + + +def add_port(ifc, system, element=None): + system.load_ports(system.get_ports(element)) + obj = system.create_empty_at_cursor_with_element_orientation(element) + port = system.run_root_assign_class(obj=obj, ifc_class="IfcDistributionPort") + ifc.run("system.assign_port", element=element, port=port) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 0e9b33457b..e91b12bebf 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -380,6 +380,7 @@ class Surveyor: @interface class System: + def create_empty_at_cursor_with_element_orientation(cls, element): pass def delete_element_objects(cls, elements): pass def disable_editing_system(cls): pass def disable_system_editing_ui(cls): pass @@ -389,6 +390,7 @@ class System: def import_system_attributes(cls, system): pass def import_systems(cls): pass def load_ports(cls, port): pass + def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): 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/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 6a64a0971e..e4f1d7c16a 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -199,7 +199,15 @@ class Drawing(blenderbim.core.tool.Drawing): ) @classmethod - def run_root_assign_class(cls, obj=None, ifc_class=None, predefined_type=None, should_add_representation=True, context=None, ifc_representation_class=None): + def run_root_assign_class( + cls, + obj=None, + ifc_class=None, + predefined_type=None, + should_add_representation=True, + context=None, + ifc_representation_class=None, + ): return blenderbim.core.root.assign_class( tool.Ifc, tool.Collector, diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index 1c5a27f0ce..e7d5051f81 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -24,6 +24,15 @@ from blenderbim.bim import import_ifc class System(blenderbim.core.tool.System): + @classmethod + def create_empty_at_cursor_with_element_orientation(cls, element): + element_obj = tool.Ifc.get_object(element) + obj = bpy.data.objects.new("Port", None) + obj.matrix_world = element_obj.matrix_world + obj.matrix_world.translation = bpy.context.scene.cursor.matrix.translation + bpy.context.scene.collection.objects.link(obj) + return obj + @classmethod def delete_element_objects(cls, elements): for element in elements: @@ -77,11 +86,34 @@ class System(blenderbim.core.tool.System): 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) + for port in ports or []: + if tool.Ifc.get_object(port): + continue + ifc_importer.create_product(port) ifc_importer.place_objects_in_collections() + @classmethod + def run_root_assign_class( + cls, + obj=None, + ifc_class=None, + predefined_type=None, + should_add_representation=True, + context=None, + ifc_representation_class=None, + ): + return blenderbim.core.root.assign_class( + tool.Ifc, + tool.Collector, + tool.Root, + obj=obj, + ifc_class=ifc_class, + predefined_type=predefined_type, + should_add_representation=should_add_representation, + context=context, + ifc_representation_class=ifc_representation_class, + ) + @classmethod def select_elements(cls, elements): for element in elements: diff --git a/src/blenderbim/test/core/test_system.py b/src/blenderbim/test/core/test_system.py index 6f47b361e5..dfed8ac817 100644 --- a/src/blenderbim/test/core/test_system.py +++ b/src/blenderbim/test/core/test_system.py @@ -103,3 +103,13 @@ class TestHidePorts: system.get_ports("element").should_be_called().will_return(["port"]) system.delete_element_objects(["port"]).should_be_called() subject.hide_ports(system, element="element") + + +class TestAddPort: + def test_run(self, ifc, system): + system.get_ports("element").should_be_called().will_return(["port"]) + system.load_ports(["port"]).should_be_called() + system.create_empty_at_cursor_with_element_orientation("element").should_be_called().will_return("obj") + system.run_root_assign_class(obj="obj", ifc_class="IfcDistributionPort").should_be_called().will_return("port") + ifc.run("system.assign_port", element="element", port="port").should_be_called() + subject.add_port(ifc, system, element="element") diff --git a/src/blenderbim/test/tool/test_system.py b/src/blenderbim/test/tool/test_system.py index c6c698ec2b..a3eec4d20c 100644 --- a/src/blenderbim/test/tool/test_system.py +++ b/src/blenderbim/test/tool/test_system.py @@ -29,6 +29,18 @@ class TestImplementsTool(NewFile): assert isinstance(subject(), blenderbim.core.tool.System) +class TestCreateEmptyAtCursorWithElementOrientation(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) + bpy.context.scene.cursor.matrix.translation[0] = 5 + obj = subject.create_empty_at_cursor_with_element_orientation(element) + assert obj.matrix_world.translation[0] == 5 + + class TestDeleteElementObjects(NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -164,6 +176,11 @@ class TestLoadPorts(NewFile): assert list(obj.location) == [0, 0, 0] +class TestRunAssignClassOperator(NewFile): + def test_nothing(self): + pass + + class TestSelectElements(NewFile): def test_run(self): ifc = ifcopenshell.file() diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/assign_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/assign_port.py index 2473301327..016e0a238f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/assign_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/assign_port.py @@ -18,6 +18,7 @@ import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.placement class Usecase: @@ -40,8 +41,14 @@ class Usecase: if self.settings["port"] in rel.RelatedObjects: return - if not rels: - return self.file.create_entity( + if rels: + rel = rels[0] + related_objects = set(rel.RelatedObjects) or set() + related_objects.add(self.settings["port"]) + rel.RelatedObjects = list(related_objects) + ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + else: + rel = self.file.create_entity( "IfcRelNests", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), @@ -49,21 +56,31 @@ class Usecase: RelatingObject=self.settings["element"], ) - rel = rels[0] - related_objects = set(rel.RelatedObjects) or set() - related_objects.add(self.settings["port"]) - rel.RelatedObjects = list(related_objects) - ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel}) + self.update_port_placement() + return rel def execute_ifc2x3(self): for rel in self.settings["element"].HasPorts or []: if rel.RelatingPort == self.settings["port"]: return - return self.file.create_entity( + rel = self.file.create_entity( "IfcRelConnectsPortToElement", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), RelatingPort=self.settings["port"], RelatedElement=self.settings["element"], ) + self.update_port_placement() + return rel + + def update_port_placement(self): + placement = getattr(self.settings["port"], "ObjectPlacement", None) + if placement and placement.is_a("IfcLocalPlacement"): + ifcopenshell.api.run( + "geometry.edit_object_placement", + self.file, + product=self.settings["port"], + matrix=ifcopenshell.util.placement.get_local_placement(self.settings["port"].ObjectPlacement), + is_si=False, + ) diff --git a/src/ifcopenshell-python/test/api/system/test_assign_port.py b/src/ifcopenshell-python/test/api/system/test_assign_port.py index b610d463fa..836222298c 100644 --- a/src/ifcopenshell-python/test/api/system/test_assign_port.py +++ b/src/ifcopenshell-python/test/api/system/test_assign_port.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import numpy import test.bootstrap import ifcopenshell.api import ifcopenshell.util.system @@ -31,6 +32,38 @@ class TestAssignPort(test.bootstrap.IFC4): ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) assert ifcopenshell.util.system.get_ports(element) == [port] + def test_updating_the_placement_to_be_relative_if_it_exists(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + ifcopenshell.api.run("unit.assign_unit", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcChiller") + subelement = ifcopenshell.api.run("system.add_port", self.file) + matrix = numpy.array( + ( + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 1.0), + (0.0, 0.0, 1.0, 1.0), + (0.0, 0.0, 0.0, 1.0), + ) + ) + submatrix = numpy.array( + ( + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 2.0), + (0.0, 0.0, 1.0, 3.0), + (0.0, 0.0, 0.0, 1.0), + ) + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", self.file, product=subelement, matrix=submatrix.copy(), is_si=False + ) + ifcopenshell.api.run("system.assign_port", self.file, element=element, port=subelement) + assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement), matrix) + assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), submatrix) + assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement + class TestAssignPortIFC2X3(test.bootstrap.IFC2X3): def test_assigning_a_port_once_only(self): @@ -41,3 +74,35 @@ class TestAssignPortIFC2X3(test.bootstrap.IFC2X3): assert ifcopenshell.util.system.get_ports(element) == [port] ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) assert ifcopenshell.util.system.get_ports(element) == [port] + + def test_updating_the_placement_to_be_relative_if_it_exists(self): + ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + ifcopenshell.api.run("unit.assign_unit", self.file) + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowSegment") + subelement = ifcopenshell.api.run("system.add_port", self.file) + matrix = numpy.array( + ( + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 1.0), + (0.0, 0.0, 1.0, 1.0), + (0.0, 0.0, 0.0, 1.0), + ) + ) + submatrix = numpy.array( + ( + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 2.0), + (0.0, 0.0, 1.0, 3.0), + (0.0, 0.0, 0.0, 1.0), + ) + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", self.file, product=element, matrix=matrix.copy(), is_si=False + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", self.file, product=subelement, matrix=submatrix.copy(), is_si=False + ) + ifcopenshell.api.run("system.assign_port", self.file, element=element, port=subelement) + assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement), matrix) + assert numpy.array_equal(ifcopenshell.util.placement.get_local_placement(subelement.ObjectPlacement), submatrix) + assert subelement.ObjectPlacement.PlacementRelTo == element.ObjectPlacement