From 60f8d30fc15393e493c49982fab5b72cc7789fe5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 16 Nov 2023 19:15:06 +0500 Subject: [PATCH] Tests for connecting mep elements and regenerating them Also: - fixed bug with bim.mep_connect_elements not considering actual blender blender object's position - moved syncing ifc position with blender object position to tools (sync_object_ifc_position) - fixed a bug in adding a bend that occured if either any of the start/end points of the start/end segments matched - in that case one of the segments end up not connected to the bend --- .../blenderbim/bim/module/model/mep.py | 20 ++--- .../blenderbim/bim/module/system/operator.py | 3 + src/blenderbim/blenderbim/tool/model.py | 6 ++ src/blenderbim/blenderbim/tool/system.py | 21 +++-- src/blenderbim/test/bim/feature/model.feature | 21 +++-- .../test/bim/feature/system.feature | 83 ++++++++++++++++++- src/blenderbim/test/bim/test_feature.py | 14 +++- 7 files changed, 138 insertions(+), 30 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index 8b387342fb..cc3daa610d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -114,10 +114,8 @@ class RegenerateDistributionElement(bpy.types.Operator, tool.Ifc.Operator): obj = tool.Ifc.get_object(element) obj_pred = tool.Ifc.get_object(predecessor) - if tool.Ifc.is_moved(obj): - blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - if tool.Ifc.is_moved(obj_pred): - blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj_pred) + tool.Model.sync_object_ifc_position(obj) + tool.Model.sync_object_ifc_position(obj_pred) port, port_pred = get_connected_ports_between(element, predecessor) port_matrix_pred = tool.Model.get_element_matrix(port_pred) @@ -990,11 +988,13 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): start_object_rotation = start_object.matrix_world.to_quaternion().to_matrix() start_segment_data = MEPGenerator().get_segment_data(start_element) end_segment_data = MEPGenerator().get_segment_data(end_element) + # use id() to match by the exact vector objects and not by their values + # since vectors position could match points_ports_map = { - start_segment_data["start_point"]: start_segment_data["start_port"], - start_segment_data["end_point"]: start_segment_data["end_port"], - end_segment_data["start_point"]: end_segment_data["start_port"], - end_segment_data["end_point"]: end_segment_data["end_port"], + id(start_segment_data["start_point"]): start_segment_data["start_port"], + id(start_segment_data["end_point"]): start_segment_data["end_port"], + id(end_segment_data["start_point"]): end_segment_data["start_port"], + id(end_segment_data["end_point"]): end_segment_data["end_port"], } get_z_basis = lambda o: tool.Cad.get_basis_vector(o, 2) @@ -1012,8 +1012,8 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): # start_/end_segment_sign indicate # whether segments' z axes are directed towards the bend - start_port = points_ports_map[start_point] - end_port = points_ports_map[end_point] + start_port = points_ports_map[id(start_point)] + end_port = points_ports_map[id(end_point)] start_point_on_origin = start_point == start_segment_data["start_point"] start_connection = "ATSTART" if start_point_on_origin else "ATEND" start_segment_sign = -1 if start_point_on_origin else 1 diff --git a/src/blenderbim/blenderbim/bim/module/system/operator.py b/src/blenderbim/blenderbim/bim/module/system/operator.py index cef76d5979..f1902914e8 100644 --- a/src/blenderbim/blenderbim/bim/module/system/operator.py +++ b/src/blenderbim/blenderbim/bim/module/system/operator.py @@ -234,6 +234,9 @@ class MEPConnectElements(bpy.types.Operator, Operator): obj1 = context.active_object obj2 = next(o for o in context.selected_objects if o != obj1) + tool.Model.sync_object_ifc_position(obj1) + tool.Model.sync_object_ifc_position(obj2) + el1 = tool.Ifc.get_entity(obj1) el2 = tool.Ifc.get_entity(obj2) diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 89db72daf4..dacc901646 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -853,6 +853,12 @@ class Model(blenderbim.core.tool.Model): return tool.Ifc.run("geometry.edit_object_placement", product=element, matrix=matrix, is_si=True) + @classmethod + def sync_object_ifc_position(cls, obj): + """make sure IFC position will be in sync with the Blender object position, if object was moved in Blender""" + if tool.Ifc.is_moved(obj): + blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + @classmethod def get_element_matrix(cls, element, keep_local=False): placement = element.ObjectPlacement diff --git a/src/blenderbim/blenderbim/tool/system.py b/src/blenderbim/blenderbim/tool/system.py index 4a2c148c24..68a50fb46a 100644 --- a/src/blenderbim/blenderbim/tool/system.py +++ b/src/blenderbim/blenderbim/tool/system.py @@ -42,8 +42,7 @@ class System(blenderbim.core.tool.System): # make sure obj.dimensions and .matrix_world has valid data bpy.context.view_layer.update() # need to make sure .ObjectPlacement is also updated when we're going to add ports - if tool.Ifc.is_moved(obj): - blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + tool.Model.sync_object_ifc_position(obj) mep_element = tool.Ifc.get_entity(obj) bbox = tool.Blender.get_object_bounding_box(obj) @@ -350,20 +349,24 @@ class System(blenderbim.core.tool.System): return decoration_data @classmethod - def get_connected_elements(cls, element, elements=None): - if elements is None: - elements = set((element,)) + def get_connected_elements(cls, element, traversed_elements=None): + """Recursively retrieves all connected elements to the given `element`. + + `traversed_elements` is a set to store connected elements fetched recursively, + should be `None`""" + if traversed_elements is None: + traversed_elements = set((element,)) connected_elements = ifcopenshell.util.system.get_connected_from(element) connected_elements += ifcopenshell.util.system.get_connected_to(element) for element in connected_elements: - if element in elements: + if element in traversed_elements: continue - elements.add(element) - cls.get_connected_elements(element, elements) + traversed_elements.add(element) + cls.get_connected_elements(element, traversed_elements) - return elements + return traversed_elements @classmethod def is_mep_element(cls, element): diff --git a/src/blenderbim/test/bim/feature/model.feature b/src/blenderbim/test/bim/feature/model.feature index 66022e0cdf..bdaa4a45f0 100644 --- a/src/blenderbim/test/bim/feature/model.feature +++ b/src/blenderbim/test/bim/feature/model.feature @@ -529,13 +529,14 @@ Scenario: Create a door, undo and create a new door Scenario: Create a MEP transition Given an empty IFC project And I create default MEP types - And the variable "element_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" + And the variable "segment_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_types}[0]" + And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I press "bim.add_constr_type_instance" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/RectSegment" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_types}[1]" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[1]" And I press "bim.add_constr_type_instance" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/CircleSegment" @@ -557,14 +558,15 @@ Scenario: Create a MEP transition Scenario: Create a MEP bend between intersecting with different locations Given an empty IFC project And I create default MEP types - And the variable "element_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" + And the variable "segment_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_types}[0]" + And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" And I press "bim.add_constr_type_instance" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_types}[0]" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I press "bim.add_constr_type_instance" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg2" And the object "IfcDuctSegment/Seg2" is rotated by "0,0,90" deg @@ -586,14 +588,15 @@ Scenario: Create a MEP bend between intersecting with different locations Scenario: Create a MEP bend between intersecting segments at the same location Given an empty IFC project And I create default MEP types - And the variable "element_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" + And the variable "segment_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_types}[0]" + And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" And I press "bim.add_constr_type_instance" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_types}[0]" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I press "bim.add_constr_type_instance" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg2" And the object "IfcDuctSegment/Seg2" is rotated by "0,0,90" deg diff --git a/src/blenderbim/test/bim/feature/system.feature b/src/blenderbim/test/bim/feature/system.feature index addd965b95..37f64d47ca 100644 --- a/src/blenderbim/test/bim/feature/system.feature +++ b/src/blenderbim/test/bim/feature/system.feature @@ -109,20 +109,101 @@ Scenario: Assign flow controls to flow element_type And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcActuator" And I press "bim.assign_class" + And I add an empty And the object "Empty" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcActuator" And I press "bim.assign_class" + And I add an empty And the object "Empty" is selected And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" And I set "scene.BIMRootProperties.ifc_class" to "IfcFlowSegment" And I press "bim.assign_class" + And the object "IfcActuator/Empty" is selected And additionally the object "IfcActuator/Empty.001" is selected And additionally the object "IfcFlowSegment/Empty" is selected When I press "bim.assign_unassign_flow_control(assign=True)" And the variable "assigned_controls" is "set(tool.System.get_flow_element_controls({ifc}.by_type('IfcFlowSegment')[0]))" - Then the variable "assigned_controls" is "set[{ifc}.by_type('IfcActuator')]" + Then the variable "assigned_controls" is "set({ifc}.by_type('IfcActuator'))" +Scenario: Connect MEP elements + Given an empty IFC project + And I create default MEP types + And the variable "segment_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" + And the variable "actuator_type_id" is "{ifc}.by_type('IfcActuatorType')[0].id()" + + # segment 1 + And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" + And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" + And I press "bim.add_constr_type_instance" + And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" + + # actuator + And I set "scene.BIMModelProperties.ifc_class" to "IfcActuatorType" + And I set "scene.BIMModelProperties.relating_type_id" to "{actuator_type_id}" + And I press "bim.add_constr_type_instance" + And the object "IfcActuator/Actuator" is moved to "10,0,0" + + # connect actuator + And the object "IfcActuator/Actuator" is selected + And additionally the object "IfcDuctSegment/Seg1" is selected + And I press "bim.mep_connect_elements" + + # final check + Then the object "IfcDuctSegment/Seg1" is at "0,0,1" + And the object "IfcDuctSegment/Seg1" dimensions are "0.4,0.2,5.0" + And the object "IfcActuator/Actuator" is at "5.5,0.0,1.0" + And the variable "connected_elements" is "set(tool.System.get_connected_elements({ifc}.by_type('IfcActuator')[0]))" + +Scenario: Connect MEP elements and regenerate + Given an empty IFC project + And I create default MEP types + And the variable "segment_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" + And the variable "actuator_type_id" is "{ifc}.by_type('IfcActuatorType')[0].id()" + + # segment1 + And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" + And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" + And I press "bim.add_constr_type_instance" + And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" + + # segment2 + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" + And I press "bim.add_constr_type_instance" + And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg2" + And the object "IfcDuctSegment/Seg2" is rotated by "0,0,90" deg + + # bend between segments 1 and 2 + And the object "IfcDuctSegment/Seg1" is selected + And additionally the object "IfcDuctSegment/Seg2" is selected + And I press "bim.mep_add_bend" + + # actuator + And I set "scene.BIMModelProperties.ifc_class" to "IfcActuatorType" + And I set "scene.BIMModelProperties.relating_type_id" to "{actuator_type_id}" + And I press "bim.add_constr_type_instance" + And the object "IfcActuator/Actuator" is moved to "10,0,0" + + # connect actuator + And the object "IfcActuator/Actuator" is selected + And additionally the object "IfcDuctSegment/Seg1" is selected + And I press "bim.mep_connect_elements" + + # move and regenerate + And the object "IfcActuator/Actuator" is moved to "0.0,5.0,10" + And the object "IfcActuator/Actuator" is selected + And I press "bim.regenerate_distribution_element" + + # final check + # And I save sample test files and open in blender + Then the object "IfcActuator/Actuator" is at "0.0,5.0,10" + And the object "IfcDuctSegment/Seg1" is at "-6.0,5.0,10.0" + And the object "IfcDuctSegment/Seg1" dimensions are "0.4,0.2,5.5" + And the object "IfcDuctFitting/DuctFitting" is at "-6.5,5.5,10.0" + And the object "IfcDuctSegment/Seg2" is at "-6.5,5.5,10.0" + And the object "IfcDuctSegment/Seg2" dimensions are "0.4,0.2,5.0" diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index 317add69f2..b906c9bc68 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -113,15 +113,27 @@ def i_load_a_new_pset_template_file(): def i_create_default_mep_types(): model_props = bpy.context.scene.BIMModelProperties + # add couple segments types model_props.type_class = "IfcDuctSegmentType" model_props.type_name = "RECT1" model_props.type_template = "FLOW_SEGMENT_RECTANGULAR" bpy.ops.bim.add_type() model_props.type_template = "FLOW_SEGMENT_CIRCULAR" - model_props.type_name = "RECT2" + model_props.type_name = "CIRCLE1" bpy.ops.bim.add_type() + # add an actuator type + model_props.type_class = "IfcActuatorType" + model_props.type_template = "MESH" # cube representation + model_props.type_name = "ACTUATOR" + bpy.ops.bim.add_type() + with bpy.context.temp_override(active_object=bpy.data.objects["IfcActuatorType/ACTUATOR"]): + bpy.ops.bim.add_port() + # port at cube's left side + bpy.data.objects["IfcDistributionPort/Port"].location = (-0.5, 0, 0) + bpy.ops.bim.hide_ports() + @given("I add a cube") @when("I add a cube")