fixed issue adding bend between segments located at the same point

also added some tests
This commit is contained in:
Andrej730
2023-11-14 17:18:57 +05:00
parent e8aa8c44e5
commit c6454803ee
4 changed files with 106 additions and 31 deletions
@@ -311,6 +311,7 @@ class MEPGenerator:
profile_joiner.set_depth(connected_obj, connected_element_length) profile_joiner.set_depth(connected_obj, connected_element_length)
def get_segment_data(self, segment): def get_segment_data(self, segment):
"""returns points data is in world space"""
ports = tool.System.get_ports(segment) ports = tool.System.get_ports(segment)
segment_object = tool.Ifc.get_object(segment) segment_object = tool.Ifc.get_object(segment)
start_point = segment_object.location 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) # TODO: profile offset may need to be flipped (check transition code)
to_start_object_space = start_object_rotation.inverted() 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) z_axis_end_object_local = to_start_object_space @ tool.Cad.get_basis_vector(end_object, 2)
def check_for_double_bends(): 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 = 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): if not tool.Cad.is_x(non_lateral_axis_offset, 0):
return ( return (
None, None,
@@ -1075,7 +1083,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
angle, rotation_axis = get_bend_rotation() 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) radial_offset = V(0, 0, 0)
ref_point_radius = self.radius + profile_dim[lateral_axis] ref_point_radius = self.radius + profile_dim[lateral_axis]
radial_offset[lateral_axis] = ref_point_radius * (1 - cos(angle)) * lateral_sign 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 = 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) end_port_offset += z_axis_end_object_local * (self.end_length * -end_segment_sign)
def get_segments_extend(): def get_segments_extend_points():
segments_intersection = segments_intersection_ws - start_point
segments_intersection = to_start_object_space @ segments_intersection
# since tangent segments are equal # since tangent segments are equal
# if drawn for the circle from the same point # if drawn for the circle from the same point
required_offset = ref_point_radius * tan(angle / 2) required_offset = ref_point_radius * tan(angle / 2)
current_start_offset = segments_intersection.length start_segment_extend_point = segments_intersection_ws - start_segment_sign * (
current_end_offset = (segments_intersection - profile_offset).length 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) return start_segment_extend_point, end_segment_extend_point
end_extend = current_end_offset - (required_offset + self.end_length)
return start_extend, end_extend
def check_new_segment_length(start_point, end_point, extend_point): def check_new_segment_length(start_point, end_point, extend_point):
"""Check if segment is placed too near to the bend 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 return None
# adjust segments to fit the radius and angle # adjust segments to fit the radius and angle
start_segment_extend, end_segment_extend = get_segments_extend() start_segment_extend_point, end_segment_extend_point = get_segments_extend_points()
start_segment_extend_point = start_point + start_segment_sign * start_segment_extend * get_z_basis(start_object)
projection = check_new_segment_length(first_segment_start, start_point, start_segment_extend_point) projection = check_new_segment_length(first_segment_start, start_point, start_segment_extend_point)
if projection is not None: if projection is not None:
self.report( self.report(
@@ -1127,7 +1132,6 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
) )
return {"ERROR"} 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) projection = check_new_segment_length(second_segment_end, end_point, end_segment_extend_point)
if projection is not None: if projection is not None:
self.report( self.report(
@@ -1148,7 +1152,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator):
self.end_length / si_conversion, self.end_length / si_conversion,
angle, angle,
self.radius / si_conversion, self.radius / si_conversion,
profile_offset / si_conversion, bend_vector / si_conversion,
flip_z_axis=start_segment_sign == -1, flip_z_axis=start_segment_sign == -1,
) )
+62 -2
View File
@@ -530,6 +530,7 @@ Scenario: Create a MEP transition
Given an empty IFC project Given an empty IFC project
And I create default MEP types 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 "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.relating_type_id" to "{element_types}[0]"
And I press "bim.add_constr_type_instance" And I press "bim.add_constr_type_instance"
And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/RectSegment" 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 moved to "2.5,0,0"
And the object "IfcDuctSegment/CircleSegment" is selected And the object "IfcDuctSegment/CircleSegment" is selected
And additionally the object "IfcDuctSegment/RectSegment" 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/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" is at "3.1299,0,0"
And the object "IfcDuctSegment/CircleSegment" dimensions are "0.1000, 0.09927, 2.370096" 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" is at "2.370096, 0.0000, 0.0000"
And the object "IfcDuctFitting/DuctFitting" dimensions are "0.4000, 0.2000, 0.759807" 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"
+14 -3
View File
@@ -28,6 +28,7 @@ from blenderbim.tool.brick import BrickStore
from blenderbim.bim.module.model.data import AuthoringData from blenderbim.bim.module.model.data import AuthoringData
from pytest_bdd import scenarios, given, when, then, parsers from pytest_bdd import scenarios, given, when, then, parsers
from mathutils import Vector from mathutils import Vector
from math import radians
scenarios("feature") scenarios("feature")
@@ -241,13 +242,23 @@ def then_the_object_name_is_selected(name):
assert obj in bpy.context.selected_objects 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}"')) @given(parsers.parse('the object "{name}" is moved to "{location}"'))
@when(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): def the_object_name_is_moved_to_location(name, location):
location = [float(co) for co in location.split(",")] location = [float(co) for co in location.split(",")]
the_object_name_exists(name).matrix_world[0][3] = location[0] obj = the_object_name_exists(name)
the_object_name_exists(name).matrix_world[1][3] = location[1] obj.matrix_world.translation = location
the_object_name_exists(name).matrix_world[2][3] = location[2]
@given(parsers.parse('the object "{name}" is scaled to "{scale}"')) @given(parsers.parse('the object "{name}" is scaled to "{scale}"'))
@@ -1172,10 +1172,9 @@ class ShapeBuilder:
""" """
print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None 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 # 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, 1)
offset = round_vector_to_precision(profile_offset, si_conversion)
diff = start_half_dim.xy - end_half_dim.xy diff = start_half_dim.xy - end_half_dim.xy
diff = Vector([abs(i) for i in diff]) diff = Vector([abs(i) for i in diff])
@@ -1320,7 +1319,7 @@ class ShapeBuilder:
end_length: float, end_length: float,
angle: float, angle: float,
radius: float, radius: float,
profile_offset: Vector, bend_vector: Vector,
flip_z_axis: bool, flip_z_axis: bool,
): ):
""" """
@@ -1332,8 +1331,9 @@ class ShapeBuilder:
:param type: float :param type: float
:param radius: bend radius :param radius: bend radius
:param type: float :param type: float
:param profile_offset: offset between start and end segments in local space of start segment :param bend_vector: offset between start and end segments in local space of start segment
used mainly to determine the seconn bend axis and it's direction. 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 type: Vector
:param flip_z_axis: since we cannot determine z axis direction from the profile offset, :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. 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") is_circular_profile = profile.is_a("IfcCircleProfileDef")
profile_dim = get_dim(profile, start_length) profile_dim = get_dim(profile, start_length)
rounded_offset = round_vector_to_precision(profile_offset, si_conversion) 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_offset[i], 0)) 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 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 z_sign = -1 if flip_z_axis else 1
rep_items = [] rep_items = []