mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
fixed issue adding bend between segments located at the same point
also added some tests
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"'))
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user