Skip to content

Commit ec48aa3

Browse files
committed
Improved robustness
1 parent d3df14e commit ec48aa3

File tree

2 files changed

+64
-86
lines changed

2 files changed

+64
-86
lines changed

src/bd_warehouse/thread.py

+61-84
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"""
2929
import copy
3030
import re
31-
import timeit
31+
from math import copysign
3232
from warnings import warn
3333
from abc import ABC, abstractmethod
3434
from typing import Literal, Optional, Tuple, Union
@@ -74,6 +74,10 @@ class Thread(BasePartObject):
7474
pitch: Length of 360° of thread rotation.
7575
length: End to end length of the thread.
7676
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.
7781
hand: Twist direction. Defaults to "right".
7882
taper_angle: Cone angle for tapered thread. Defaults to None.
7983
end_finishes: Profile of each end, one of:
@@ -108,6 +112,7 @@ def __init__(
108112
pitch: float,
109113
length: float,
110114
apex_offset: float = 0.0,
115+
interference: float = 0.2,
111116
hand: Literal["right", "left"] = "right",
112117
taper_angle: Optional[float] = None,
113118
end_finishes: Tuple[
@@ -135,21 +140,34 @@ def __init__(
135140
self.pitch = pitch
136141
self.length = length
137142
self.apex_offset = apex_offset
143+
self.interference = interference
138144
self.right_hand = hand == "right"
139145
self.end_finishes = end_finishes
140146
self.tooth_height = abs(self.apex_radius - self.root_radius)
141147
self.taper = 0 if taper_angle is None else taper_angle
142148
self.simple = simple
143149
self.thread_loops = None
144150

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+
145168
if simple:
146169
# 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))
153171
self.wrapped = TopoDS_Shape()
154172
else:
155173
# Create base cylindrical thread
@@ -178,62 +196,52 @@ def __init__(
178196

179197
bd_object = Compound(label="thread", children=loops)
180198

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
182201
# Bottom
183202
if self.end_finishes.count("chamfer") != 0:
184203
chamfer_shape = self._make_chamfer_shape()
185204
if end_finishes[0] == "fade":
186205
start_tip = self._make_fade_end(True)
187-
start_tip.label = "start_tip"
206+
start_tip.label = "bottom_tip"
188207
loops[0].joints["0"].connect_to(start_tip.joints["0"])
189208
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"]:
200210
children = list(bd_object.children)
201211
bottom_loop = children.pop(0)
202212
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)
206217
bottom_loop.label = label
207218
bd_object.children = [bottom_loop] + children
208219

209220
# Top
210221
if end_finishes[1] == "fade":
211222
end_tip = self._make_fade_end(False)
212-
end_tip.label = "end_tip"
223+
end_tip.label = "top_tip"
213224
loops[-1].joints["1"].connect_to(end_tip.joints["1"])
214225
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"]:
225227
children = list(bd_object.children)
226228
top_loops = []
227-
for _ in range(2):
229+
for _ in range(3):
228230
if not children:
229231
continue
230232
top_loop = children.pop(-1)
231233
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)
237245
bd_object.children = children + top_loops
238246

239247
super().__init__(
@@ -264,30 +272,18 @@ def _make_thread_loop(self, loop_height: float) -> Solid:
264272
height=loop_height,
265273
radius=self.root_radius,
266274
)
275+
267276
for i in range(11):
268277
u = i / 10
278+
269279
with BuildSketch(
270280
Plane(
271281
thread_path_wire @ u,
272282
x_dir=(0, 0, 1),
273283
z_dir=thread_path_wire % u,
274284
)
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)
291287
loft()
292288

293289
loop = thread_loop.part.solids()[0]
@@ -305,36 +301,22 @@ def _make_fade_end(self, bottom: bool) -> Solid:
305301
Solid: The tip of the thread fading to almost nothing
306302
"""
307303
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)
309305
with BuildPart() as fade_tip:
310-
with BuildLine() as tip_path:
306+
with BuildLine():
311307
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
315309
)
310+
316311
for i in range(11):
317312
u = i / 10
318313
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)
322315
):
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)
338320
scale(by=(11 - i) / 11)
339321
loft()
340322

@@ -478,12 +460,7 @@ def __init__(
478460
root_width = 3 * self.pitch / 4 if external else 7 * self.pitch / 8
479461
if simple:
480462
# 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))
487464
self.wrapped = TopoDS_Shape()
488465

489466
else:

tests/test_thread.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ def test_exterior_thread(self):
6262

6363
for end0 in TestIsoThread.end_finishes:
6464
for end1 in TestIsoThread.end_finishes:
65-
with self.subTest(end0=end0, end1=end1):
65+
length = (1 + random.random() * 9) * MM
66+
with self.subTest(end0=end0, end1=end1, length=length):
6667
thread = IsoThread(
6768
major_diameter=6 * MM,
6869
pitch=1 * MM,
69-
length=(1 + random.random() * 9) * MM,
70+
length=length,
7071
external=True,
7172
end_finishes=(end0, end1),
7273
hand="right",

0 commit comments

Comments
 (0)