28
28
"""
29
29
import copy
30
30
import re
31
- import timeit
31
+ from math import copysign
32
32
from warnings import warn
33
33
from abc import ABC , abstractmethod
34
34
from typing import Literal , Optional , Tuple , Union
@@ -74,6 +74,10 @@ class Thread(BasePartObject):
74
74
pitch: Length of 360° of thread rotation.
75
75
length: End to end length of the thread.
76
76
apex_offset: Asymmetric thread apex offset from center. Defaults to 0.0.
77
+ interference: Amount the thread will overlap with nut or bolt core. Used
78
+ to help create valid threaded objects where the thread must fuse
79
+ with another object. For threaded objects built as Compounds, this
80
+ value could be set to 0.0. Defaults to 0.2.
77
81
hand: Twist direction. Defaults to "right".
78
82
taper_angle: Cone angle for tapered thread. Defaults to None.
79
83
end_finishes: Profile of each end, one of:
@@ -108,6 +112,7 @@ def __init__(
108
112
pitch : float ,
109
113
length : float ,
110
114
apex_offset : float = 0.0 ,
115
+ interference : float = 0.2 ,
111
116
hand : Literal ["right" , "left" ] = "right" ,
112
117
taper_angle : Optional [float ] = None ,
113
118
end_finishes : Tuple [
@@ -135,21 +140,34 @@ def __init__(
135
140
self .pitch = pitch
136
141
self .length = length
137
142
self .apex_offset = apex_offset
143
+ self .interference = interference
138
144
self .right_hand = hand == "right"
139
145
self .end_finishes = end_finishes
140
146
self .tooth_height = abs (self .apex_radius - self .root_radius )
141
147
self .taper = 0 if taper_angle is None else taper_angle
142
148
self .simple = simple
143
149
self .thread_loops = None
144
150
151
+ # Create the thread profile
152
+ with BuildSketch (mode = Mode .PRIVATE ) as thread_face :
153
+ height = self .apex_radius - self .root_radius
154
+ overlap = - interference * copysign (1 , height )
155
+ with BuildLine () as thread_profile :
156
+ Polyline (
157
+ (self .root_width / 2 , overlap ),
158
+ (self .root_width / 2 , 0 ),
159
+ (self .apex_width / 2 + self .apex_offset , height ),
160
+ (- self .apex_width / 2 + self .apex_offset , height ),
161
+ (- self .root_width / 2 , 0 ),
162
+ (- self .root_width / 2 , overlap ),
163
+ close = True ,
164
+ )
165
+ make_face ()
166
+ self .thread_profile = thread_face .sketch_local .faces ()[0 ]
167
+
145
168
if simple :
146
169
# Initialize with a valid shape then nullify
147
- super ().__init__ (
148
- part = Solid .make_box (1 , 1 , 1 ),
149
- rotation = rotation ,
150
- align = tuplify (align , 3 ),
151
- mode = mode ,
152
- )
170
+ super ().__init__ (part = Solid .make_box (1 , 1 , 1 ))
153
171
self .wrapped = TopoDS_Shape ()
154
172
else :
155
173
# Create base cylindrical thread
@@ -178,62 +196,52 @@ def __init__(
178
196
179
197
bd_object = Compound (label = "thread" , children = loops )
180
198
181
- # Apply the end finishes
199
+ # Apply the end finishes. Note that it's significantly faster
200
+ # to just apply the end finish to a single loop then the entire Compound
182
201
# Bottom
183
202
if self .end_finishes .count ("chamfer" ) != 0 :
184
203
chamfer_shape = self ._make_chamfer_shape ()
185
204
if end_finishes [0 ] == "fade" :
186
205
start_tip = self ._make_fade_end (True )
187
- start_tip .label = "start_tip "
206
+ start_tip .label = "bottom_tip "
188
207
loops [0 ].joints ["0" ].connect_to (start_tip .joints ["0" ])
189
208
bd_object .children = list (bd_object .children ) + [start_tip ]
190
- elif end_finishes [0 ] == "square" :
191
- children = list (bd_object .children )
192
- bottom_loop = children .pop (0 )
193
- label = bottom_loop .label
194
- bottom_loop : Solid = split (
195
- bottom_loop , bisect_by = Plane .XY , keep = Keep .TOP
196
- )
197
- bottom_loop .label = label
198
- bd_object .children = [bottom_loop ] + children
199
- elif end_finishes [0 ] == "chamfer" :
209
+ elif end_finishes [0 ] in ["square" , "chamfer" ]:
200
210
children = list (bd_object .children )
201
211
bottom_loop = children .pop (0 )
202
212
label = bottom_loop .label
203
- bottom_loop : Solid = bottom_loop .intersect (chamfer_shape )
204
- if bottom_loop .volume == 0 :
205
- raise RuntimeError ("Thread construction failed" )
213
+ if end_finishes [0 ] == "square" :
214
+ bottom_loop = split (bottom_loop , bisect_by = Plane .XY , keep = Keep .TOP )
215
+ else :
216
+ bottom_loop = bottom_loop .intersect (chamfer_shape )
206
217
bottom_loop .label = label
207
218
bd_object .children = [bottom_loop ] + children
208
219
209
220
# Top
210
221
if end_finishes [1 ] == "fade" :
211
222
end_tip = self ._make_fade_end (False )
212
- end_tip .label = "end_tip "
223
+ end_tip .label = "top_tip "
213
224
loops [- 1 ].joints ["1" ].connect_to (end_tip .joints ["1" ])
214
225
bd_object .children = list (bd_object .children ) + [end_tip ]
215
- elif end_finishes [1 ] == "square" :
216
- children = list (bd_object .children )
217
- top_loop = children .pop (- 1 )
218
- label = top_loop .label
219
- top_loop : Solid = split (
220
- top_loop , bisect_by = Plane .XY .offset (self .length ), keep = Keep .BOTTOM
221
- )
222
- top_loop .label = label
223
- bd_object .children = children + [top_loop ]
224
- elif end_finishes [1 ] == "chamfer" :
226
+ elif end_finishes [1 ] in ["square" , "chamfer" ]:
225
227
children = list (bd_object .children )
226
228
top_loops = []
227
- for _ in range (2 ):
229
+ for _ in range (3 ):
228
230
if not children :
229
231
continue
230
232
top_loop = children .pop (- 1 )
231
233
label = top_loop .label
232
- top_loop = top_loop .intersect (chamfer_shape )
233
- if top_loop .volume == 0 :
234
- raise RuntimeError ("Thread construction failed" )
235
- top_loop .label = label
236
- top_loops .append (top_loop )
234
+ if end_finishes [1 ] == "square" :
235
+ top_loop : Solid = split (
236
+ top_loop ,
237
+ bisect_by = Plane .XY .offset (self .length ),
238
+ keep = Keep .BOTTOM ,
239
+ )
240
+ else :
241
+ top_loop = top_loop .intersect (chamfer_shape )
242
+ if top_loop .volume != 0 :
243
+ top_loop .label = label
244
+ top_loops .append (top_loop )
237
245
bd_object .children = children + top_loops
238
246
239
247
super ().__init__ (
@@ -264,30 +272,18 @@ def _make_thread_loop(self, loop_height: float) -> Solid:
264
272
height = loop_height ,
265
273
radius = self .root_radius ,
266
274
)
275
+
267
276
for i in range (11 ):
268
277
u = i / 10
278
+
269
279
with BuildSketch (
270
280
Plane (
271
281
thread_path_wire @ u ,
272
282
x_dir = (0 , 0 , 1 ),
273
283
z_dir = thread_path_wire % u ,
274
284
)
275
- ) as thread_face :
276
- with BuildLine () as thread_profile :
277
- Polyline (
278
- (self .root_width / 2 , 0 ),
279
- (
280
- self .apex_width / 2 + self .apex_offset ,
281
- self .apex_radius - self .root_radius ,
282
- ),
283
- (
284
- - self .apex_width / 2 + self .apex_offset ,
285
- self .apex_radius - self .root_radius ,
286
- ),
287
- (- self .root_width / 2 , 0 ),
288
- close = True ,
289
- )
290
- make_face ()
285
+ ):
286
+ add (self .thread_profile )
291
287
loft ()
292
288
293
289
loop = thread_loop .part .solids ()[0 ]
@@ -305,36 +301,22 @@ def _make_fade_end(self, bottom: bool) -> Solid:
305
301
Solid: The tip of the thread fading to almost nothing
306
302
"""
307
303
dir = - 1 if bottom else 1
308
- fade_apex_offset = - self .apex_offset if bottom else self .apex_offset
304
+ height = min ( self .pitch / 4 , self .length / 2 )
309
305
with BuildPart () as fade_tip :
310
- with BuildLine () as tip_path :
306
+ with BuildLine ():
311
307
fade_path_wire = Helix (
312
- pitch = self .pitch ,
313
- height = dir * self .pitch / 4 ,
314
- radius = self .root_radius ,
308
+ pitch = self .pitch , height = dir * height , radius = self .root_radius
315
309
)
310
+
316
311
for i in range (11 ):
317
312
u = i / 10
318
313
with BuildSketch (
319
- Plane (
320
- fade_path_wire @ u , x_dir = (0 , 0 , dir ), z_dir = fade_path_wire % u
321
- )
314
+ Plane (fade_path_wire @ u , x_dir = (0 , 0 , 1 ), z_dir = fade_path_wire % u )
322
315
):
323
- with BuildLine () as thread_profile :
324
- Polyline (
325
- (self .root_width / 2 , 0 ),
326
- (
327
- self .apex_width / 2 + fade_apex_offset ,
328
- self .apex_radius - self .root_radius ,
329
- ),
330
- (
331
- - self .apex_width / 2 + fade_apex_offset ,
332
- self .apex_radius - self .root_radius ,
333
- ),
334
- (- self .root_width / 2 , 0 ),
335
- close = True ,
336
- )
337
- make_face ()
316
+ if bottom :
317
+ add (mirror (self .thread_profile , about = Plane .XZ ))
318
+ else :
319
+ add (self .thread_profile )
338
320
scale (by = (11 - i ) / 11 )
339
321
loft ()
340
322
@@ -478,12 +460,7 @@ def __init__(
478
460
root_width = 3 * self .pitch / 4 if external else 7 * self .pitch / 8
479
461
if simple :
480
462
# Initialize with a valid shape then nullify
481
- super ().__init__ (
482
- part = Solid .make_box (1 , 1 , 1 ),
483
- rotation = rotation ,
484
- align = tuplify (align , 3 ),
485
- mode = mode ,
486
- )
463
+ super ().__init__ (part = Solid .make_box (1 , 1 , 1 ))
487
464
self .wrapped = TopoDS_Shape ()
488
465
489
466
else :
0 commit comments