diff --git a/src/blenderbim/blenderbim/bim/module/model/mep.py b/src/blenderbim/blenderbim/bim/module/model/mep.py index f171dec3e7..8b387342fb 100644 --- a/src/blenderbim/blenderbim/bim/module/model/mep.py +++ b/src/blenderbim/blenderbim/bim/module/model/mep.py @@ -311,6 +311,7 @@ class MEPGenerator: profile_joiner.set_depth(connected_obj, connected_element_length) def get_segment_data(self, segment): + """returns points data is in world space""" ports = tool.System.get_ports(segment) segment_object = tool.Ifc.get_object(segment) start_point = segment_object.location @@ -1025,7 +1026,14 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): # TODO: profile offset may need to be flipped (check transition code) to_start_object_space = start_object_rotation.inverted() - profile_offset = (to_start_object_space @ end_point) - (to_start_object_space @ start_point) + ref_point = end_point.copy() + end_segment_dir = (second_segment_end - end_point).normalized() + # we prioritize direction between end_point and start_point for bend_vector + # if those point match we use general end segment direction + if tool.Cad.is_x((end_point - start_point).length, 0): + ref_point = end_point + end_segment_dir + bend_vector = (to_start_object_space @ ref_point) - (to_start_object_space @ start_point) + z_axis_end_object_local = to_start_object_space @ tool.Cad.get_basis_vector(end_object, 2) def check_for_double_bends(): @@ -1049,7 +1057,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): ) non_lateral_axis = 0 if lateral_axes[0] == 1 else 1 - non_lateral_axis_offset = profile_offset[non_lateral_axis] + non_lateral_axis_offset = bend_vector[non_lateral_axis] if not tool.Cad.is_x(non_lateral_axis_offset, 0): return ( None, @@ -1075,7 +1083,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): angle, rotation_axis = get_bend_rotation() - lateral_sign = tool.Cad.sign(profile_offset[lateral_axis]) + lateral_sign = tool.Cad.sign(bend_vector[lateral_axis]) radial_offset = V(0, 0, 0) ref_point_radius = self.radius + profile_dim[lateral_axis] radial_offset[lateral_axis] = ref_point_radius * (1 - cos(angle)) * lateral_sign @@ -1083,21 +1091,19 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): end_port_offset = radial_offset + V(0, 0, self.start_length * start_segment_sign) end_port_offset += z_axis_end_object_local * (self.end_length * -end_segment_sign) - def get_segments_extend(): - segments_intersection = segments_intersection_ws - start_point - segments_intersection = to_start_object_space @ segments_intersection - + def get_segments_extend_points(): # since tangent segments are equal # if drawn for the circle from the same point required_offset = ref_point_radius * tan(angle / 2) - current_start_offset = segments_intersection.length - current_end_offset = (segments_intersection - profile_offset).length + start_segment_extend_point = segments_intersection_ws - start_segment_sign * ( + self.start_length + required_offset + ) * get_z_basis(start_object) + end_segment_extend_point = segments_intersection_ws - end_segment_sign * ( + self.end_length + required_offset + ) * get_z_basis(end_object) - start_extend = current_start_offset - (required_offset + self.start_length) - end_extend = current_end_offset - (required_offset + self.end_length) - - return start_extend, end_extend + return start_segment_extend_point, end_segment_extend_point def check_new_segment_length(start_point, end_point, extend_point): """Check if segment is placed too near to the bend point. @@ -1117,8 +1123,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): return None # adjust segments to fit the radius and angle - start_segment_extend, end_segment_extend = get_segments_extend() - start_segment_extend_point = start_point + start_segment_sign * start_segment_extend * get_z_basis(start_object) + start_segment_extend_point, end_segment_extend_point = get_segments_extend_points() projection = check_new_segment_length(first_segment_start, start_point, start_segment_extend_point) if projection is not None: self.report( @@ -1127,7 +1132,6 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): ) return {"ERROR"} - end_segment_extend_point = end_point + end_segment_sign * end_segment_extend * get_z_basis(end_object) projection = check_new_segment_length(second_segment_end, end_point, end_segment_extend_point) if projection is not None: self.report( @@ -1148,7 +1152,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): self.end_length / si_conversion, angle, self.radius / si_conversion, - profile_offset / si_conversion, + bend_vector / si_conversion, flip_z_axis=start_segment_sign == -1, ) diff --git a/src/blenderbim/test/bim/feature/model.feature b/src/blenderbim/test/bim/feature/model.feature index d49c455625..66022e0cdf 100644 --- a/src/blenderbim/test/bim/feature/model.feature +++ b/src/blenderbim/test/bim/feature/model.feature @@ -530,6 +530,7 @@ 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 I set "scene.BIMModelProperties.relating_type_id" to "{element_types}[0]" And I press "bim.add_constr_type_instance" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/RectSegment" @@ -542,11 +543,70 @@ Scenario: Create a MEP transition And the object "IfcDuctSegment/CircleSegment" is moved to "2.5,0,0" And the object "IfcDuctSegment/CircleSegment" is selected And additionally the object "IfcDuctSegment/RectSegment" is selected - And I press "bim.mep_add_transition()" + And I press "bim.mep_add_transition" - Then the object "IfcDuctSegment/RectSegment" is at "0,0,0" + Then the object "IfcDuctFitting/DuctFitting" exists + And the object "IfcDuctFittingType/Transition" exists + And the object "IfcDuctSegment/RectSegment" is at "0,0,0" And the object "IfcDuctSegment/RectSegment" dimensions are "0.4,0.2,2.370096" And the object "IfcDuctSegment/CircleSegment" is at "3.1299,0,0" And the object "IfcDuctSegment/CircleSegment" dimensions are "0.1000, 0.09927, 2.370096" And the object "IfcDuctFitting/DuctFitting" is at "2.370096, 0.0000, 0.0000" And the object "IfcDuctFitting/DuctFitting" dimensions are "0.4000, 0.2000, 0.759807" + +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 I set "scene.BIMModelProperties.relating_type_id" to "{element_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 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 + + And the object "IfcDuctSegment/Seg2" is moved to "6,1,1" + And the object "IfcDuctSegment/Seg1" is selected + And additionally the object "IfcDuctSegment/Seg2" is selected + And I press "bim.mep_add_bend" + + Then the object "IfcDuctFitting/DuctFitting" exists + And the object "IfcDuctFittingType/Bend" exists + And the object "IfcDuctSegment/Seg1" is at "0,0,1.0" + And the object "IfcDuctSegment/Seg1" dimensions are "0.4,0.2,5.5" + And the object "IfcDuctSegment/Seg2" is at "6.0,0.5,1.0" + And the object "IfcDuctSegment/Seg2" dimensions are "0.4,0.2,5.5" + And the object "IfcDuctFitting/DuctFitting" is at "6.0, 0.5, 1.0" + And the object "IfcDuctFitting/DuctFitting" dimensions are "0.7, 0.2, 0.7" + +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 I set "scene.BIMModelProperties.relating_type_id" to "{element_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 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 + + And the object "IfcDuctSegment/Seg1" is selected + And additionally the object "IfcDuctSegment/Seg2" is selected + And I press "bim.mep_add_bend" + + Then the object "IfcDuctFitting/DuctFitting" exists + And the object "IfcDuctFittingType/Bend" exists + And the object "IfcDuctSegment/Seg1" is at "0.5,0,1.0" + And the object "IfcDuctSegment/Seg1" dimensions are "0.4,0.2,4.5" + And the object "IfcDuctSegment/Seg2" is at "0.0,0.5,1.0" + And the object "IfcDuctSegment/Seg2" dimensions are "0.4,0.2,4.5" + And the object "IfcDuctFitting/DuctFitting" is at "0.0, 0.5, 1.0" + And the object "IfcDuctFitting/DuctFitting" dimensions are "0.7, 0.2, 0.7" diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index 87787caf66..317add69f2 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -28,6 +28,7 @@ from blenderbim.tool.brick import BrickStore from blenderbim.bim.module.model.data import AuthoringData from pytest_bdd import scenarios, given, when, then, parsers from mathutils import Vector +from math import radians scenarios("feature") @@ -241,13 +242,23 @@ def then_the_object_name_is_selected(name): assert obj in bpy.context.selected_objects +@given(parsers.parse('the object "{name}" is rotated by "{rotation_deg}" deg')) +@when(parsers.parse('the object "{name}" is rotated by "{rotation_deg}" deg')) +def the_object_name_is_rotated_by(name, rotation_deg): + rotation_deg = [radians(float(rot)) for rot in rotation_deg.split(",")] + obj = the_object_name_exists(name) + obj.rotation_euler[0] += rotation_deg[0] + obj.rotation_euler[1] += rotation_deg[1] + obj.rotation_euler[2] += rotation_deg[2] + bpy.context.view_layer.update() # make sure matrix is updated + + @given(parsers.parse('the object "{name}" is moved to "{location}"')) @when(parsers.parse('the object "{name}" is moved to "{location}"')) def the_object_name_is_moved_to_location(name, location): location = [float(co) for co in location.split(",")] - the_object_name_exists(name).matrix_world[0][3] = location[0] - the_object_name_exists(name).matrix_world[1][3] = location[1] - the_object_name_exists(name).matrix_world[2][3] = location[2] + obj = the_object_name_exists(name) + obj.matrix_world.translation = location @given(parsers.parse('the object "{name}" is scaled to "{scale}"')) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 6941e37b2f..3467b90401 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -1172,10 +1172,9 @@ class ShapeBuilder: """ print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None - # offsets tend to have bunch of float point garbage + # vectors tend to have bunch of float point garbage # that can result in errors when we're calculating value for square root below - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(self.file) - offset = round_vector_to_precision(profile_offset, si_conversion) + offset = round_vector_to_precision(profile_offset, 1) diff = start_half_dim.xy - end_half_dim.xy diff = Vector([abs(i) for i in diff]) @@ -1320,7 +1319,7 @@ class ShapeBuilder: end_length: float, angle: float, radius: float, - profile_offset: Vector, + bend_vector: Vector, flip_z_axis: bool, ): """ @@ -1332,8 +1331,9 @@ class ShapeBuilder: :param type: float :param radius: bend radius :param type: float - :param profile_offset: offset between start and end segments in local space of start segment - used mainly to determine the seconn bend axis and it's direction. + :param bend_vector: offset between start and end segments in local space of start segment + used mainly to determine the second bend axis and it's direction (positive or negative), + the actual magnitude of the vector is not important (though near zero values will be ignored). :param type: Vector :param flip_z_axis: since we cannot determine z axis direction from the profile offset, there is an option to flip it if bend is going by start segment Z- axis. @@ -1359,10 +1359,10 @@ class ShapeBuilder: is_circular_profile = profile.is_a("IfcCircleProfileDef") profile_dim = get_dim(profile, start_length) - rounded_offset = round_vector_to_precision(profile_offset, si_conversion) - lateral_axis = next(i for i in range(2) if not is_x(rounded_offset[i], 0)) + rounded_bend_vector = round_vector_to_precision(bend_vector, si_conversion) + lateral_axis = next(i for i in range(2) if not is_x(rounded_bend_vector[i], 0)) non_lateral_axis = 1 if lateral_axis == 0 else 0 - lateral_sign = sign(profile_offset[lateral_axis]) + lateral_sign = sign(bend_vector[lateral_axis]) z_sign = -1 if flip_z_axis else 1 rep_items = []