Skip to content

Commit 04f5683

Browse files
committed
-- Fixed off-by-one bug in screw_thread.py, removed comments.
-- Updated unit test.
1 parent 06233f5 commit 04f5683

File tree

2 files changed

+30
-32
lines changed

2 files changed

+30
-32
lines changed

solid/screw_thread.py

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def thread( outline_pts, inner_rad, pitch, length, external=True, segments_per_r
3434
section of the thread
3535
3636
inner_rad: radius of cylinder the screw will wrap around
37-
pitch: height for one revolution
37+
pitch: height for one revolution; must be <= the height of outline_pts
38+
bounding box to avoid self-intersection
3839
length: distance from bottom-most point of screw to topmost
3940
external: if True, the cross-section is external to a cylinder. If False,
4041
the segment is internal to it, and outline_pts will be
@@ -45,16 +46,20 @@ def thread( outline_pts, inner_rad, pitch, length, external=True, segments_per_r
4546
neck_out_degrees: degrees through which outer edge of the screw thread will move from
4647
full thickness back to zero
4748
48-
NOTE: if pitch is less than the height of each tooth (outline_pts), OpenSCAD will likely
49-
crash, since the resulting screw would self-intersect all over the place
49+
NOTE: if pitch is less than the or equal to the height of each tooth (outline_pts),
50+
OpenSCAD will likely crash, since the resulting screw would self-intersect
51+
all over the place. For screws with essentially no space between
52+
threads, (i.e., pitch=tooth_height), I use pitch= tooth_height+EPSILON,
53+
since pitch=tooth_height will self-intersect for rotations >=1
5054
'''
5155
a = union()
5256
rotations = float(length)/pitch
5357

5458
total_angle = 360.0*rotations
5559
up_step = float(length) / (rotations*segments_per_rot)
56-
total_steps = int(ceil( rotations * segments_per_rot))
57-
step_angle = total_angle/ total_steps
60+
# Add one to total_steps so we have total_steps *segments*
61+
total_steps = int(ceil( rotations * segments_per_rot)) + 1
62+
step_angle = total_angle/ (total_steps -1)
5863

5964
all_points = []
6065
all_tris = []
@@ -85,6 +90,7 @@ def thread( outline_pts, inner_rad, pitch, length, external=True, segments_per_r
8590

8691
for i in range( total_steps):
8792
angle = i*step_angle
93+
8894
elevation = i*up_step
8995
if angle > total_angle:
9096
angle = total_angle
@@ -100,19 +106,9 @@ def thread( outline_pts, inner_rad, pitch, length, external=True, segments_per_r
100106
elif angle > total_angle - neck_in_degrees:
101107
rad = neck_in_rad + int_ext_mult * (total_angle - angle)/neck_out_degrees * outline_w
102108

103-
# if angle < neck_in_degrees:
104-
# if external:
105-
# rad = min_rad + angle/neck_in_degrees * outline_w
106-
# else:
107-
# rad = max_rad - angle/neck_out_degrees *outline_w
108-
# elif angle > total_angle - neck_out_degrees:
109-
# if external:
110-
# rad = min_rad + (total_angle - angle)/neck_out_degrees * outline_w
111-
# else:
112-
# rad = max_rad - (total_angle - angle)/neck_out_degrees *outline_w
113-
#
114109
elev_vec = Vector3( rad, 0, elevation)
115110

111+
# create new points
116112
for p in euc_points:
117113
pt = (p + elev_vec).rotate_around( axis=euc_up, theta=radians( angle))
118114
all_points.append( pt.as_arr())
@@ -137,21 +133,19 @@ def thread( outline_pts, inner_rad, pitch, length, external=True, segments_per_r
137133
a = polyhedron( points=all_points, triangles=all_tris)
138134

139135
if external:
140-
# Subtract the center, to remove the neck-in pieces
141-
# subtract above and below to make sure the entire screw fits within height 'length'
142-
cube_side = 2*(inner_rad + outline_w)
143-
subs = union()(
144-
down( outline_h/2)( cylinder( inner_rad, length + outline_h)),
145-
down( cube_side/2)( cube( cube_side, center=True)),
146-
up( cube_side/2 + length)( cube( cube_side, center=True))
147-
)
148-
a -= subs
136+
# Intersect with a cylindrical tube to make sure we fit into
137+
# the correct dimensions
138+
tube = cylinder( r=inner_rad+outline_w+EPSILON, h=length, segments=segments_per_rot)
139+
tube -= cylinder( r=inner_rad, h=length, segments=segments_per_rot)
149140
else:
150141
# If the threading is internal, intersect with a central cylinder
151142
# to make sure nothing else remains
152-
a = a* cylinder( r=inner_rad, h=length, segments=segments_per_rot)
143+
tube = cylinder( r=inner_rad, h=length, segments=segments_per_rot)
144+
a *= tube
153145
return render()(a)
154146

147+
148+
155149
def default_thread_section( tooth_height, tooth_depth):
156150
# An isoceles triangle, tooth_height vertically, tooth_depth wide:
157151
res = [ [ 0, -tooth_height/2],

solid/test/test_screw_thread.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,29 @@
88
from solid import *
99
from solid.screw_thread import thread, default_thread_section
1010

11+
SEGMENTS = 8
1112
class TestScrewThread( unittest.TestCase):
1213
def test_thread( self):
1314
tooth_height = 10
1415
tooth_depth = 5
1516
outline = default_thread_section( tooth_height=tooth_height, tooth_depth=tooth_depth)
16-
actual_obj = thread( outline_pts=outline, inner_rad=20, pitch=2*tooth_height, length=2*tooth_height, segments_per_rot=4,
17-
neck_in_degrees=0, neck_out_degrees=0)
17+
actual_obj = thread( outline_pts=outline, inner_rad=20, pitch=tooth_height,
18+
length=0.75*tooth_height, segments_per_rot=SEGMENTS,
19+
neck_in_degrees=45, neck_out_degrees=45)
1820
actual = scad_render( actual_obj)
19-
expected = '\n\nrender() {\n\tdifference() {\n\t\tpolyhedron(points = [[20.0000000000, 0.0000000000, -5.0000000000], [25.0000000000, 0.0000000000, 0.0000000000], [20.0000000000, 0.0000000000, 5.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 25.0000000000, 5.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [-20.0000000000, 0.0000000000, 5.0000000000], [-25.0000000000, 0.0000000000, 10.0000000000], [-20.0000000000, 0.0000000000, 15.0000000000], [-0.0000000000, -20.0000000000, 10.0000000000], [-0.0000000000, -25.0000000000, 15.0000000000], [-0.0000000000, -20.0000000000, 20.0000000000]], triangles = [[0, 1, 3], [1, 4, 3], [1, 2, 4], [2, 5, 4], [0, 5, 2], [0, 3, 5], [3, 4, 6], [4, 7, 6], [4, 5, 7], [5, 8, 7], [3, 8, 5], [3, 6, 8], [6, 7, 9], [7, 10, 9], [7, 8, 10], [8, 11, 10], [6, 11, 8], [6, 9, 11], [0, 2, 1], [9, 10, 11]]);\n\t\tunion() {\n\t\t\ttranslate(v = [0, 0, -5]) {\n\t\t\t\tcylinder(h = 30, r = 20);\n\t\t\t}\n\t\t\ttranslate(v = [0, 0, -25]) {\n\t\t\t\tcube(center = true, size = 50);\n\t\t\t}\n\t\t\ttranslate(v = [0, 0, 45]) {\n\t\t\t\tcube(center = true, size = 50);\n\t\t\t}\n\t\t}\n\t}\n}'
21+
expected = '\n\nrender() {\n\tintersection() {\n\t\tpolyhedron(points = [[14.9900000000, 0.0000000000, -5.0000000000], [19.9900000000, 0.0000000000, 0.0000000000], [14.9900000000, 0.0000000000, 5.0000000000], [14.1421356237, 14.1421356237, -3.7500000000], [17.6776695297, 17.6776695297, 1.2500000000], [14.1421356237, 14.1421356237, 6.2500000000], [0.0000000000, 20.0000000000, -2.5000000000], [0.0000000000, 25.0000000000, 2.5000000000], [0.0000000000, 20.0000000000, 7.5000000000], [-14.1421356237, 14.1421356237, -1.2500000000], [-17.6776695297, 17.6776695297, 3.7500000000], [-14.1421356237, 14.1421356237, 8.7500000000], [-20.0000000000, 0.0000000000, 0.0000000000], [-25.0000000000, 0.0000000000, 5.0000000000], [-20.0000000000, 0.0000000000, 10.0000000000], [-14.1421356237, -14.1421356237, 1.2500000000], [-17.6776695297, -17.6776695297, 6.2500000000], [-14.1421356237, -14.1421356237, 11.2500000000], [-0.0000000000, -14.9900000000, 2.5000000000], [-0.0000000000, -19.9900000000, 7.5000000000], [-0.0000000000, -14.9900000000, 12.5000000000]], triangles = [[0, 1, 3], [1, 4, 3], [1, 2, 4], [2, 5, 4], [0, 5, 2], [0, 3, 5], [3, 4, 6], [4, 7, 6], [4, 5, 7], [5, 8, 7], [3, 8, 5], [3, 6, 8], [6, 7, 9], [7, 10, 9], [7, 8, 10], [8, 11, 10], [6, 11, 8], [6, 9, 11], [9, 10, 12], [10, 13, 12], [10, 11, 13], [11, 14, 13], [9, 14, 11], [9, 12, 14], [12, 13, 15], [13, 16, 15], [13, 14, 16], [14, 17, 16], [12, 17, 14], [12, 15, 17], [15, 16, 18], [16, 19, 18], [16, 17, 19], [17, 20, 19], [15, 20, 17], [15, 18, 20], [0, 2, 1], [18, 19, 20]]);\n\t\tdifference() {\n\t\t\tcylinder($fn = 8, h = 7.5000000000, r = 25.0100000000);\n\t\t\tcylinder($fn = 8, h = 7.5000000000, r = 20);\n\t\t}\n\t}\n}'
2022
self.assertEqual( expected, actual)
2123

2224
def test_thread_internal( self):
2325
tooth_height = 10
2426
tooth_depth = 5
2527
outline = default_thread_section( tooth_height=tooth_height, tooth_depth=tooth_depth)
26-
actual_obj = thread( outline_pts=outline, inner_rad=20, pitch=2*tooth_height, length=2*tooth_height,
27-
external=False, segments_per_rot=4,neck_in_degrees=0, neck_out_degrees=0)
28+
actual_obj = thread( outline_pts=outline, inner_rad=20, pitch=2*tooth_height,
29+
length=2*tooth_height, segments_per_rot=SEGMENTS,
30+
neck_in_degrees=45, neck_out_degrees=45,
31+
external=False)
2832
actual = scad_render( actual_obj)
29-
expected = '\n\nrender() {\n\tintersection() {\n\t\tpolyhedron(points = [[20.0000000000, 0.0000000000, -5.0000000000], [15.0000000000, 0.0000000000, 0.0000000000], [20.0000000000, 0.0000000000, 5.0000000000], [0.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 15.0000000000, 5.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [-20.0000000000, 0.0000000000, 5.0000000000], [-15.0000000000, 0.0000000000, 10.0000000000], [-20.0000000000, 0.0000000000, 15.0000000000], [-0.0000000000, -20.0000000000, 10.0000000000], [-0.0000000000, -15.0000000000, 15.0000000000], [-0.0000000000, -20.0000000000, 20.0000000000]], triangles = [[0, 1, 3], [1, 4, 3], [1, 2, 4], [2, 5, 4], [0, 5, 2], [0, 3, 5], [3, 4, 6], [4, 7, 6], [4, 5, 7], [5, 8, 7], [3, 8, 5], [3, 6, 8], [6, 7, 9], [7, 10, 9], [7, 8, 10], [8, 11, 10], [6, 11, 8], [6, 9, 11], [0, 2, 1], [9, 10, 11]]);\n\t\tcylinder($fn = 4, h = 20, r = 20);\n\t}\n}'
33+
expected = '\n\nrender() {\n\tintersection() {\n\t\tpolyhedron(points = [[25.0100000000, 0.0000000000, -5.0000000000], [20.0100000000, 0.0000000000, 0.0000000000], [25.0100000000, 0.0000000000, 5.0000000000], [14.1421356237, 14.1421356237, -2.5000000000], [10.6066017178, 10.6066017178, 2.5000000000], [14.1421356237, 14.1421356237, 7.5000000000], [0.0000000000, 20.0000000000, 0.0000000000], [0.0000000000, 15.0000000000, 5.0000000000], [0.0000000000, 20.0000000000, 10.0000000000], [-14.1421356237, 14.1421356237, 2.5000000000], [-10.6066017178, 10.6066017178, 7.5000000000], [-14.1421356237, 14.1421356237, 12.5000000000], [-20.0000000000, 0.0000000000, 5.0000000000], [-15.0000000000, 0.0000000000, 10.0000000000], [-20.0000000000, 0.0000000000, 15.0000000000], [-14.1421356237, -14.1421356237, 7.5000000000], [-10.6066017178, -10.6066017178, 12.5000000000], [-14.1421356237, -14.1421356237, 17.5000000000], [-0.0000000000, -20.0000000000, 10.0000000000], [-0.0000000000, -15.0000000000, 15.0000000000], [-0.0000000000, -20.0000000000, 20.0000000000], [14.1421356237, -14.1421356237, 12.5000000000], [10.6066017178, -10.6066017178, 17.5000000000], [14.1421356237, -14.1421356237, 22.5000000000], [25.0100000000, -0.0000000000, 15.0000000000], [20.0100000000, -0.0000000000, 20.0000000000], [25.0100000000, -0.0000000000, 25.0000000000]], triangles = [[0, 1, 3], [1, 4, 3], [1, 2, 4], [2, 5, 4], [0, 5, 2], [0, 3, 5], [3, 4, 6], [4, 7, 6], [4, 5, 7], [5, 8, 7], [3, 8, 5], [3, 6, 8], [6, 7, 9], [7, 10, 9], [7, 8, 10], [8, 11, 10], [6, 11, 8], [6, 9, 11], [9, 10, 12], [10, 13, 12], [10, 11, 13], [11, 14, 13], [9, 14, 11], [9, 12, 14], [12, 13, 15], [13, 16, 15], [13, 14, 16], [14, 17, 16], [12, 17, 14], [12, 15, 17], [15, 16, 18], [16, 19, 18], [16, 17, 19], [17, 20, 19], [15, 20, 17], [15, 18, 20], [18, 19, 21], [19, 22, 21], [19, 20, 22], [20, 23, 22], [18, 23, 20], [18, 21, 23], [21, 22, 24], [22, 25, 24], [22, 23, 25], [23, 26, 25], [21, 26, 23], [21, 24, 26], [0, 2, 1], [24, 25, 26]]);\n\t\tcylinder($fn = 8, h = 20, r = 20);\n\t}\n}'
3034
self.assertEqual( expected, actual)
3135

3236
def test_default_thread_section( self):

0 commit comments

Comments
 (0)