b3dkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
b3dkit/dovetail.py ADDED
@@ -0,0 +1,1338 @@
1
+ from enum import Enum, auto
2
+ from math import atan, atan2, degrees, radians, tan
3
+ from typing import Tuple
4
+ from build123d import (
5
+ Align,
6
+ Axis,
7
+ BuildLine,
8
+ BuildPart,
9
+ BuildSketch,
10
+ Box,
11
+ Compound,
12
+ Cylinder,
13
+ FilletPolyline,
14
+ GridLocations,
15
+ Line,
16
+ Location,
17
+ Mode,
18
+ Part,
19
+ Plane,
20
+ PolarLocations,
21
+ Polyline,
22
+ add,
23
+ extrude,
24
+ fillet,
25
+ loft,
26
+ make_face,
27
+ )
28
+
29
+ # it's a bad habit, but I keep some simple test code under __main__
30
+ # to make creating test object easy -- this adds ".b3dkit" to the path
31
+ if __name__ == "__main__":
32
+ import sys, os
33
+
34
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
35
+
36
+
37
+ from b3dkit.point import (
38
+ Point,
39
+ midpoint,
40
+ shifted_midpoint,
41
+ )
42
+
43
+ from b3dkit.click_fit import Divot
44
+
45
+
46
+ class DovetailPart(Enum):
47
+ TAIL = auto()
48
+ SOCKET = auto()
49
+
50
+
51
+ class DovetailStyle(Enum):
52
+ TRADITIONAL = auto()
53
+ SNUGTAIL = auto()
54
+ T_SLOT = auto()
55
+
56
+
57
+ def subpart_outline_boundary(
58
+ start: Point,
59
+ end: Point,
60
+ max_dimension: float,
61
+ section: DovetailPart = DovetailPart.TAIL,
62
+ tolerance: float = 0.1,
63
+ scarf_offset: float = 0,
64
+ ) -> Line:
65
+ direction_multiplier = 1 if section == DovetailPart.TAIL else -1
66
+ base_angle = start.angle_to(end)
67
+ dovetail_tolerance = -(abs(tolerance / 2)) * direction_multiplier
68
+ adjusted_start_point = start.related_point(base_angle - 90, scarf_offset)
69
+ adjusted_end_point = end.related_point(base_angle - 90, scarf_offset)
70
+ toleranced_start_point = adjusted_start_point.related_point(
71
+ base_angle - 90, dovetail_tolerance
72
+ )
73
+ toleranced_end_point = adjusted_end_point.related_point(
74
+ base_angle - 90, dovetail_tolerance
75
+ )
76
+
77
+ with BuildLine() as border:
78
+ Polyline(
79
+ *[
80
+ tuple(toleranced_start_point),
81
+ tuple(
82
+ toleranced_start_point.related_point(
83
+ base_angle + 180, max_dimension
84
+ )
85
+ ),
86
+ tuple(
87
+ toleranced_start_point.related_point(
88
+ base_angle - 225 * direction_multiplier, max_dimension
89
+ )
90
+ ),
91
+ tuple(
92
+ toleranced_end_point.related_point(
93
+ base_angle + 45 * direction_multiplier, max_dimension
94
+ )
95
+ ),
96
+ tuple(toleranced_end_point.related_point(base_angle, max_dimension)),
97
+ tuple(toleranced_end_point),
98
+ ]
99
+ )
100
+
101
+ return border.line
102
+
103
+
104
+ def snugtail_subpart_outline(
105
+ start: Point,
106
+ end: Point,
107
+ max_dimension: float = 1000,
108
+ section: DovetailPart = DovetailPart.TAIL,
109
+ tolerance: float = 0.025,
110
+ tail_angle_offset: float = 15,
111
+ taper_distance: float = 0,
112
+ length_ratio: float = 0.8,
113
+ depth_ratio: float = 0.15,
114
+ scarf_offset: float = 0,
115
+ straighten_dovetail: bool = False,
116
+ ) -> Line:
117
+ """
118
+ given a part and a start and end point on the XY plane, returns an outline to build an intersection Part to generate the subpart
119
+ args:
120
+ - part: the part to split
121
+ - start: the start point along the XY Plane for the dovetail line
122
+ - end: the end point along the XY Plane for the dovetail line
123
+ - section: the section of the dovetail to create (DovetailPart.TAIL or DovetailPart.SOCKET)
124
+ - tolerance: the tolerance for the split
125
+ - tail_angle_offset: the adjustment pitch of angle of the dovetail (0 will result in a square dovetail)
126
+ - taper_distance: an extra shrinking factor for the dovetail size, allows for easier assembly
127
+ - length_ratio: the ratio of the length of the tongue to the total length of the dovetail
128
+ - depth_ratio: the ratio of the depth of the tongue to the total length of the dovetail
129
+ - scarf_offset: setting this to a non-zero value will shift the dovetail to allow for tilt adjustemnt between the top & bottom outlines
130
+ - straighten_dovetail: setting this to True will draw the straight line of the cut,
131
+ allowing for the correct tolerances for the section
132
+ """
133
+ if (length_ratio + depth_ratio > 1) and not straighten_dovetail:
134
+ raise ValueError(
135
+ "the combined length_ratio and depth_ratio must be not exceed 1"
136
+ )
137
+
138
+ direction_multiplier = 1 if section == DovetailPart.TAIL else -1
139
+ base_angle = start.angle_to(end)
140
+ opposite_angle = 180 if base_angle == 0 else -base_angle
141
+ dovetail_tolerance = -(abs(tolerance / 2)) * direction_multiplier
142
+ adjusted_start_point = start.related_point(base_angle - 90, scarf_offset)
143
+ adjusted_end_point = end.related_point(base_angle - 90, scarf_offset)
144
+ toleranced_start_point = adjusted_start_point.related_point(
145
+ base_angle - 90, dovetail_tolerance
146
+ )
147
+ toleranced_end_point = adjusted_end_point.related_point(
148
+ base_angle - 90, dovetail_tolerance
149
+ )
150
+
151
+ cut_length = start.distance_to(end)
152
+ tail_depth = cut_length * depth_ratio
153
+ tail_length = cut_length * length_ratio
154
+
155
+ cut_start = toleranced_start_point
156
+ cut_end = toleranced_end_point
157
+
158
+ fin_join = cut_start.related_point(
159
+ base_angle, tail_depth / 2 + dovetail_tolerance
160
+ ).related_point(base_angle - 90, tail_depth / 2 + dovetail_tolerance)
161
+ fin_depart = cut_end.related_point(
162
+ base_angle - 180, tail_depth / 2 + dovetail_tolerance
163
+ ).related_point(base_angle - 90, tail_depth / 2 + dovetail_tolerance)
164
+
165
+ start_fin = cut_start.related_point(
166
+ base_angle, tail_depth / 2 + dovetail_tolerance
167
+ ).related_point(base_angle + 90, dovetail_tolerance)
168
+ end_fin = cut_end.related_point(
169
+ base_angle - 180, tail_depth / 2 + dovetail_tolerance
170
+ ).related_point(base_angle + 90, dovetail_tolerance)
171
+
172
+ start_snugtail = start_fin.related_point(
173
+ base_angle + 90, cut_length - tail_depth / 2
174
+ )
175
+ end_snugtail = end_fin.related_point(base_angle + 90, cut_length - tail_depth / 2)
176
+
177
+ fin_connect = start_fin.related_point(
178
+ base_angle + 90, cut_length - dovetail_tolerance
179
+ )
180
+
181
+ fin_disconnect = end_fin.related_point(
182
+ base_angle + 90, cut_length - dovetail_tolerance
183
+ )
184
+
185
+ start_tail_line = fin_connect.related_point(
186
+ base_angle,
187
+ abs(dovetail_tolerance) * (4 if section == DovetailPart.TAIL else 6)
188
+ - dovetail_tolerance * 2,
189
+ )
190
+
191
+ end_tail_line = fin_disconnect.related_point(
192
+ opposite_angle,
193
+ abs(dovetail_tolerance) * (4 if section == DovetailPart.TAIL else 6)
194
+ - dovetail_tolerance * 2,
195
+ )
196
+
197
+ with BuildLine() as tail_line:
198
+ add(
199
+ subpart_outline_boundary(
200
+ start=start,
201
+ end=end,
202
+ max_dimension=max_dimension,
203
+ section=section,
204
+ tolerance=tolerance,
205
+ scarf_offset=scarf_offset,
206
+ )
207
+ )
208
+
209
+ FilletPolyline(
210
+ *[cut_start, fin_join, start_fin],
211
+ radius=abs(dovetail_tolerance) * (3 if section == DovetailPart.TAIL else 2),
212
+ )
213
+ if straighten_dovetail:
214
+ Line(start_fin, start_snugtail)
215
+ else:
216
+ add(
217
+ dovetail_split_line(
218
+ start=start_fin.related_point(base_angle, -dovetail_tolerance),
219
+ end=start_snugtail.related_point(base_angle, -dovetail_tolerance),
220
+ linear_offset=-tail_depth / 2,
221
+ section=section,
222
+ tolerance=tolerance,
223
+ tail_angle_offset=tail_angle_offset,
224
+ taper_distance=taper_distance,
225
+ length_ratio=length_ratio,
226
+ depth_ratio=depth_ratio,
227
+ )
228
+ )
229
+ FilletPolyline(
230
+ *[start_snugtail, fin_connect, start_tail_line],
231
+ radius=abs(dovetail_tolerance) * (2 if section == DovetailPart.TAIL else 3),
232
+ )
233
+ if straighten_dovetail:
234
+ Line(
235
+ start_tail_line,
236
+ end_tail_line,
237
+ )
238
+ else:
239
+ add(
240
+ dovetail_split_line(
241
+ start=start_tail_line.related_point(
242
+ base_angle - 90, -dovetail_tolerance
243
+ ),
244
+ end=end_tail_line.related_point(
245
+ base_angle - 90, -dovetail_tolerance
246
+ ),
247
+ section=section,
248
+ linear_offset=0,
249
+ tolerance=tolerance,
250
+ tail_angle_offset=tail_angle_offset,
251
+ taper_distance=taper_distance,
252
+ length_ratio=length_ratio,
253
+ depth_ratio=depth_ratio,
254
+ )
255
+ )
256
+ FilletPolyline(
257
+ *[end_tail_line, fin_disconnect, end_snugtail],
258
+ radius=abs(dovetail_tolerance) * (2 if section == DovetailPart.TAIL else 3),
259
+ )
260
+ if straighten_dovetail:
261
+ Line(end_snugtail, end_fin)
262
+ else:
263
+ add(
264
+ dovetail_split_line(
265
+ start=end_snugtail.related_point(base_angle, dovetail_tolerance),
266
+ end=end_fin.related_point(base_angle, dovetail_tolerance),
267
+ section=section,
268
+ linear_offset=tail_depth / 2,
269
+ tolerance=tolerance,
270
+ tail_angle_offset=tail_angle_offset,
271
+ taper_distance=taper_distance,
272
+ length_ratio=length_ratio,
273
+ depth_ratio=depth_ratio,
274
+ )
275
+ )
276
+ FilletPolyline(
277
+ *[end_fin, fin_depart, cut_end],
278
+ radius=abs(dovetail_tolerance) * (3 if section == DovetailPart.TAIL else 2),
279
+ )
280
+ return tail_line.line
281
+
282
+
283
+ def dovetail_subpart_outline(
284
+ start: Point,
285
+ end: Point,
286
+ max_dimension: float = 1000,
287
+ section: DovetailPart = DovetailPart.TAIL,
288
+ style: DovetailStyle = DovetailStyle.TRADITIONAL,
289
+ linear_offset: float = 0,
290
+ tolerance: float = 0.025,
291
+ tail_angle_offset: float = 15,
292
+ taper_distance: float = 0,
293
+ length_ratio: float = 1 / 3,
294
+ depth_ratio: float = 1 / 6,
295
+ slot_count: int = 1,
296
+ depth: float = 2,
297
+ scarf_offset: float = 0,
298
+ straighten_dovetail: bool = False,
299
+ ) -> Line:
300
+ """
301
+ given a part and a start and end point on the XY plane, returns an outline to build an intersection Part to generate the subpart
302
+ args:
303
+ - part: the part to split
304
+ - start: the start point along the XY Plane for the dovetail line
305
+ - end: the end point along the XY Plane for the dovetail line
306
+ - section: the section of the dovetail to create (DovetailPart.TAIL or DovetailPart.SOCKET)
307
+ - style: valid styles are DovetailStyle.TRADITIONAL, DovetailStyle.T_SLOT
308
+ - linear_offset: offsets the center of the tail or socket along the line by the ammount specified
309
+ - tolerance: the tolerance for the split
310
+ - tail_angle_offset: the adjustment pitch of angle of the dovetail (0 will result in a square dovetail)
311
+ - taper_distance: an extra shrinking factor for the dovetail size, allows for easier assembly
312
+ - length_ratio: the ratio of the length of the tongue to the total length of the dovetail
313
+ - depth_ratio: the ratio of the depth of the tongue to the total length of the dovetail
314
+ - scarf_offset: setting this to a non-zero value will shift the dovetail to allow for tilt adjustemnt between the top & bottom outlines
315
+ - straighten_dovetail: setting this to True will draw the straight line of the cut,
316
+ allowing for the correct tolerances for the section
317
+ """
318
+ if style not in (DovetailStyle.TRADITIONAL, DovetailStyle.T_SLOT):
319
+ raise ValueError(f"Invalid style: {style}")
320
+ direction_multiplier = 1 if section == DovetailPart.TAIL else -1
321
+ base_angle = start.angle_to(end)
322
+ dovetail_tolerance = -(abs(tolerance / 2)) * direction_multiplier
323
+ adjusted_start_point = start.related_point(base_angle - 90, scarf_offset)
324
+ adjusted_end_point = end.related_point(base_angle - 90, scarf_offset)
325
+ toleranced_start_point = adjusted_start_point.related_point(
326
+ base_angle - 90, dovetail_tolerance
327
+ )
328
+ toleranced_end_point = adjusted_end_point.related_point(
329
+ base_angle - 90, dovetail_tolerance
330
+ )
331
+
332
+ with BuildLine() as tail_line:
333
+ add(
334
+ subpart_outline_boundary(
335
+ start=start,
336
+ end=end,
337
+ max_dimension=max_dimension,
338
+ section=section,
339
+ tolerance=tolerance,
340
+ scarf_offset=scarf_offset,
341
+ )
342
+ )
343
+ if straighten_dovetail:
344
+ Line(toleranced_start_point, toleranced_end_point)
345
+ elif style == DovetailStyle.T_SLOT:
346
+ add(
347
+ tslot_split_line(
348
+ start=adjusted_start_point,
349
+ end=adjusted_end_point,
350
+ section=section,
351
+ slot_count=slot_count,
352
+ depth=depth,
353
+ tolerance=tolerance,
354
+ taper_distance=taper_distance,
355
+ )
356
+ )
357
+ else:
358
+ add(
359
+ dovetail_split_line(
360
+ start=adjusted_start_point,
361
+ end=adjusted_end_point,
362
+ section=section,
363
+ linear_offset=linear_offset,
364
+ tolerance=tolerance,
365
+ tail_angle_offset=tail_angle_offset,
366
+ taper_distance=taper_distance,
367
+ length_ratio=length_ratio,
368
+ depth_ratio=depth_ratio,
369
+ )
370
+ )
371
+ return tail_line.line
372
+
373
+
374
+ def subpart_outline(
375
+ start: Point,
376
+ end: Point,
377
+ max_dimension: float = 1000,
378
+ section: DovetailPart = DovetailPart.TAIL,
379
+ style: DovetailStyle = DovetailStyle.SNUGTAIL,
380
+ linear_offset: float = 0,
381
+ tolerance: float = 0.025,
382
+ tail_angle_offset: float = 15,
383
+ taper_distance: float = 0,
384
+ length_ratio: float = 1 / 3,
385
+ depth_ratio: float = 1 / 6,
386
+ scarf_offset: float = 0,
387
+ slot_count: int = 2,
388
+ depth: float = 2,
389
+ straighten_dovetail: bool = False,
390
+ ) -> Line:
391
+ """
392
+ given a part and a start and end point on the XY plane, returns an outline to build an intersection Part to generate the subpart
393
+ args:
394
+ - start: the start point along the XY Plane for the dovetail line
395
+ - end: the end point along the XY Plane for the dovetail line
396
+ - max_dimension: the maximum dimension of the part to split, used to determine the size of the outline
397
+ - section: the section of the dovetail to create (DovetailPart.TAIL or DovetailPart.SOCKET)
398
+ - linear_offset: offsets the center of the tail or socket along the line by the ammount specified
399
+ - tolerance: the tolerance for the split
400
+ - tail_angle_offset: the adjustment pitch of angle of the dovetail (0 will result in a square dovetail)
401
+ - taper_distance: an extra shrinking factor for the dovetail size, allows for easier assembly
402
+ - length_ratio: the ratio of the length of the tongue to the total length of the dovetail
403
+ - depth_ratio: the ratio of the depth of the tongue to the total length of the dovetail
404
+ - scarf_offset: setting this to a non-zero value will shift the dovetail to allow for tilt adjustemnt between the top & bottom outlines
405
+ - straighten_dovetail: setting this to True will draw the straight line of the cut,
406
+ allowing for the correct tolerances for the section
407
+ """
408
+ if style == DovetailStyle.SNUGTAIL:
409
+ return snugtail_subpart_outline(
410
+ start=start,
411
+ end=end,
412
+ max_dimension=max_dimension,
413
+ section=section,
414
+ tolerance=tolerance,
415
+ tail_angle_offset=tail_angle_offset,
416
+ taper_distance=taper_distance,
417
+ length_ratio=length_ratio,
418
+ scarf_offset=scarf_offset,
419
+ straighten_dovetail=straighten_dovetail,
420
+ )
421
+ else:
422
+ return dovetail_subpart_outline(
423
+ start=start,
424
+ end=end,
425
+ max_dimension=max_dimension,
426
+ section=section,
427
+ style=style,
428
+ linear_offset=linear_offset,
429
+ tolerance=tolerance,
430
+ tail_angle_offset=tail_angle_offset,
431
+ taper_distance=taper_distance,
432
+ length_ratio=length_ratio,
433
+ depth_ratio=depth_ratio,
434
+ scarf_offset=scarf_offset,
435
+ slot_count=slot_count,
436
+ depth=depth,
437
+ straighten_dovetail=straighten_dovetail,
438
+ )
439
+
440
+
441
+ def traditional_subpart_divots(
442
+ subpart: Part,
443
+ start: Point,
444
+ end: Point,
445
+ section: DovetailPart = DovetailPart.TAIL,
446
+ linear_offset: float = 0,
447
+ tolerance: float = 0.025,
448
+ vertical_tolerance: float = 0.2,
449
+ scarf_angle: float = 0,
450
+ taper_angle: float = 0,
451
+ depth_ratio: float = 1 / 6,
452
+ vertical_offset: float = 0,
453
+ click_fit_radius: float = 0,
454
+ ):
455
+ """
456
+ adds/subtracts click-fit divots to subpart and returns it
457
+ ----------
458
+ Arguments:
459
+ - subpart: the part to add divots to
460
+ - start: the start point along the XY Plane for the dovetail line
461
+ - end: the end point along the XY Plane for the dovetail line
462
+ - section: the section of the dovetail to create (DovetailPart.TAIL or DovetailPart.SOCKET)
463
+ - linear_offset: offsets the center of the tail or socket along the line by the ammount specified
464
+ - tolerance: the tolerance for the split
465
+ - scarf_angle: the scarf angle of the dovetail
466
+ - taper_angle: an extra shrinking factor for the dovetail size, allows for easier assembly
467
+ - length_ratio: the ratio of the length of the tongue to the total length of the dovetail
468
+ - vertical_offset: the vertical offset of the dovetail
469
+ - click_fit_radius: the radius of the click-fit divots
470
+ """
471
+
472
+ cut_angle = start.angle_to(end)
473
+
474
+ # how much of an offset is there along the top and bottom of the subparts
475
+ scarf_offset = (subpart.bounding_box().size.Z) * tan(radians(scarf_angle)) / 2
476
+
477
+ tailtop_z = subpart.bounding_box().max.Z + (
478
+ vertical_offset if vertical_offset < 0 else 0
479
+ )
480
+
481
+ adjusted_top_divot_angle = scarf_angle - taper_angle
482
+
483
+ taper_offset = (subpart.bounding_box().size.Z - abs(vertical_offset)) * tan(
484
+ radians(adjusted_top_divot_angle)
485
+ )
486
+
487
+ topmode = (
488
+ Mode.SUBTRACT
489
+ if ((section == DovetailPart.TAIL) == (vertical_offset < 0))
490
+ else Mode.ADD
491
+ )
492
+ bottommode = (
493
+ Mode.ADD
494
+ if ((section == DovetailPart.SOCKET) == (vertical_offset >= 0))
495
+ else Mode.SUBTRACT
496
+ )
497
+
498
+ top_divot_center = shifted_midpoint(start, end, linear_offset).related_point(
499
+ cut_angle - 90,
500
+ start.distance_to(end) * depth_ratio
501
+ - scarf_offset
502
+ + taper_offset
503
+ - click_fit_radius / 2,
504
+ )
505
+
506
+ with BuildPart() as divotedpart:
507
+ add(subpart, mode=Mode.ADD)
508
+ with BuildPart(
509
+ Location(
510
+ (
511
+ top_divot_center.x,
512
+ top_divot_center.y,
513
+ tailtop_z - click_fit_radius * 2,
514
+ )
515
+ ),
516
+ mode=topmode,
517
+ ):
518
+ add(
519
+ Divot(
520
+ click_fit_radius,
521
+ positive=topmode == Mode.ADD,
522
+ extend_base=True,
523
+ )
524
+ .rotate(
525
+ Axis.X,
526
+ (90 * (-1 if vertical_offset < 0 else 1))
527
+ + adjusted_top_divot_angle,
528
+ )
529
+ .rotate(Axis.Y, cut_angle),
530
+ )
531
+ #####################################
532
+ # Bottom divots
533
+ #####################################
534
+ start_side = start.related_point(cut_angle, click_fit_radius * 2).related_point(
535
+ cut_angle + 90,
536
+ scarf_offset - ((click_fit_radius * 2) * tan(radians(scarf_angle))),
537
+ )
538
+ end_side = end.related_point(cut_angle, -click_fit_radius * 2).related_point(
539
+ cut_angle - 90,
540
+ -scarf_offset + ((click_fit_radius * 2) * tan(radians(scarf_angle))),
541
+ )
542
+ with BuildPart(
543
+ Location(
544
+ (
545
+ start_side.x,
546
+ start_side.y,
547
+ click_fit_radius * 2,
548
+ )
549
+ ),
550
+ mode=bottommode,
551
+ ):
552
+ add(
553
+ Divot(
554
+ click_fit_radius,
555
+ positive=bottommode == Mode.ADD,
556
+ extend_base=True,
557
+ )
558
+ .rotate(
559
+ Axis.X,
560
+ (90 * (1 if vertical_offset < 0 else -1)) + scarf_angle,
561
+ )
562
+ .rotate(Axis.Y, cut_angle),
563
+ )
564
+ with BuildPart(
565
+ Location((end_side.x, end_side.y, click_fit_radius * 2)),
566
+ mode=bottommode,
567
+ ):
568
+ add(
569
+ Divot(click_fit_radius, positive=True, extend_base=True)
570
+ .rotate(
571
+ Axis.X,
572
+ (90 * (1 if vertical_offset < 0 else -1)) + scarf_angle,
573
+ )
574
+ .rotate(Axis.Y, cut_angle),
575
+ )
576
+
577
+ return divotedpart.part
578
+
579
+
580
+ def snugtail_divots(
581
+ subpart: Part,
582
+ start: Point,
583
+ end: Point,
584
+ section: DovetailPart = DovetailPart.TAIL,
585
+ tolerance: float = 0.025,
586
+ scarf_angle: float = 0,
587
+ depth_ratio: float = 1 / 10,
588
+ length_ratio: float = 1 / 5,
589
+ vertical_offset: float = 0,
590
+ click_fit_radius: float = 0,
591
+ ) -> Part:
592
+ part_width = start.distance_to(end)
593
+ tail_depth = part_width * depth_ratio
594
+ direction_multiplier = -1 if section == DovetailPart.TAIL else 1
595
+ inner_width = (
596
+ part_width - (part_width * depth_ratio * 2) - (tolerance * direction_multiplier)
597
+ )
598
+ cut_angle = start.angle_to(end)
599
+ with BuildPart() as divotedpart:
600
+ add(subpart, mode=Mode.ADD)
601
+ with BuildPart(
602
+ Location(
603
+ (
604
+ 0,
605
+ part_width * length_ratio * 1.5 + midpoint(start, end).Y,
606
+ click_fit_radius * 2,
607
+ )
608
+ ),
609
+ mode=Mode.SUBTRACT if section == DovetailPart.SOCKET else Mode.ADD,
610
+ ):
611
+ with PolarLocations(inner_width / 2, 2, start_angle=cut_angle):
612
+ Divot(
613
+ radius=click_fit_radius,
614
+ positive=True,
615
+ extend_base=True,
616
+ ).rotate(Axis.Y, -90)
617
+ return divotedpart.part
618
+
619
+
620
+ def subpart_divots(
621
+ subpart: Part,
622
+ start: Point,
623
+ end: Point,
624
+ section: DovetailPart = DovetailPart.TAIL,
625
+ style: DovetailStyle = DovetailStyle.SNUGTAIL,
626
+ linear_offset: float = 0,
627
+ tolerance: float = 0.025,
628
+ vertical_tolerance: float = 0.2,
629
+ scarf_angle: float = 0,
630
+ taper_angle: float = 0,
631
+ depth_ratio: float = 1 / 6,
632
+ length_ratio: float = 1 / 3,
633
+ vertical_offset: float = 0,
634
+ click_fit_radius: float = 0,
635
+ ):
636
+ """
637
+ adds/subtracts click-fit divots to subpart and returns it
638
+ ----------
639
+ Arguments:
640
+ - subpart: the part to add divots to
641
+ - start: the start point along the XY Plane for the dovetail line
642
+ - end: the end point along the XY Plane for the dovetail line
643
+ - section: the section of the dovetail to create (DovetailPart.TAIL or DovetailPart.SOCKET)
644
+ - style: create a traditional dovetal or a cut that wraps around 3 sides of the object and creates a tighter fit
645
+ - linear_offset: offsets the center of the tail or socket along the line by the ammount specified
646
+ - tolerance: the tolerance for the split
647
+ - scarf_angle: the scarf angle of the dovetail
648
+ - taper_angle: an extra shrinking factor for the dovetail size, allows for easier assembly
649
+ - depth_ratio: the ratio of the depth of the tongue to the total length of the dovetail
650
+ - length_ratio: the ratio of the length of the tongue to the total length of the dovetail
651
+ - vertical_offset: the vertical offset of the dovetail
652
+ - click_fit_radius: the radius of the click-fit divots
653
+ """
654
+ if style == DovetailStyle.TRADITIONAL:
655
+ return traditional_subpart_divots(
656
+ subpart=subpart,
657
+ start=start,
658
+ end=end,
659
+ section=section,
660
+ linear_offset=linear_offset,
661
+ tolerance=tolerance,
662
+ vertical_tolerance=vertical_tolerance,
663
+ scarf_angle=scarf_angle,
664
+ taper_angle=taper_angle,
665
+ depth_ratio=depth_ratio,
666
+ vertical_offset=vertical_offset,
667
+ click_fit_radius=click_fit_radius,
668
+ )
669
+ else:
670
+ return snugtail_divots(
671
+ subpart=subpart,
672
+ start=start,
673
+ end=end,
674
+ section=section,
675
+ tolerance=tolerance,
676
+ scarf_angle=scarf_angle,
677
+ depth_ratio=1 / 10,
678
+ length_ratio=1 / 5,
679
+ vertical_offset=vertical_offset,
680
+ click_fit_radius=click_fit_radius,
681
+ )
682
+
683
+
684
+ def subpart_section(
685
+ start: Point,
686
+ end: Point,
687
+ max_dimension: float,
688
+ section: DovetailPart = DovetailPart.TAIL,
689
+ style: DovetailStyle = DovetailStyle.SNUGTAIL,
690
+ floor_z: float = 0,
691
+ floor_taper_distance: float = 0,
692
+ floor_scarf_offset: float = 0,
693
+ top_z: float = 0,
694
+ top_taper_distance: float = 0,
695
+ top_scarf_offset: float = 0,
696
+ tolerance: float = 0.1,
697
+ slot_count: int = 1,
698
+ depth: float = 2,
699
+ linear_offset: float = 0,
700
+ tail_angle_offset: float = 20,
701
+ length_ratio: float = 1 / 3,
702
+ depth_ratio: float = 1 / 6,
703
+ straighten_dovetail: bool = False,
704
+ ) -> Part:
705
+ with BuildPart() as intersect:
706
+ with BuildSketch(Plane.XY.offset(floor_z)):
707
+ with BuildLine() as baseline:
708
+ add(
709
+ subpart_outline(
710
+ start=start,
711
+ end=end,
712
+ max_dimension=max_dimension,
713
+ section=section,
714
+ style=style,
715
+ tolerance=tolerance,
716
+ taper_distance=floor_taper_distance,
717
+ slot_count=slot_count,
718
+ depth=depth,
719
+ scarf_offset=floor_scarf_offset,
720
+ straighten_dovetail=straighten_dovetail,
721
+ )
722
+ )
723
+ make_face()
724
+ with BuildSketch(Plane.XY.offset(top_z)) as topline:
725
+ with BuildLine():
726
+ add(
727
+ subpart_outline(
728
+ start=start,
729
+ end=end,
730
+ max_dimension=max_dimension,
731
+ section=section,
732
+ style=style,
733
+ tolerance=tolerance,
734
+ taper_distance=top_taper_distance,
735
+ slot_count=slot_count,
736
+ depth=depth,
737
+ scarf_offset=top_scarf_offset,
738
+ straighten_dovetail=straighten_dovetail,
739
+ )
740
+ )
741
+ make_face()
742
+ loft()
743
+ return intersect.part
744
+
745
+
746
+ def dovetail_subpart(
747
+ part: Part,
748
+ start: Point,
749
+ end: Point,
750
+ section: DovetailPart = DovetailPart.TAIL,
751
+ style: DovetailStyle = DovetailStyle.SNUGTAIL,
752
+ linear_offset: float = 0,
753
+ tolerance: float = 0.025,
754
+ vertical_tolerance: float = 0.2,
755
+ slot_count: int = 1,
756
+ depth: float = 2,
757
+ tail_angle_offset: float = 15,
758
+ taper_angle: float = 0,
759
+ length_ratio: float = 1 / 3,
760
+ depth_ratio: float = 1 / 6,
761
+ scarf_angle: float = 0,
762
+ vertical_offset: float = 0,
763
+ click_fit_radius: float = 0,
764
+ ) -> Part:
765
+ """
766
+ given a part and a start and end point on the XY plane, returns a Part for the appropriate split section
767
+ args:
768
+ - part: the part to split
769
+ - start: the start point along the XY Plane for the dovetail line
770
+ - end: the end point along the XY Plane for the dovetail line
771
+ - section: the section of the dovetail to create (DovetailPart.TAIL or DovetailPart.SOCKET)
772
+ - style: create a traditional dovetal or a cut that wraps around 3 sides of the object and creates a tighter fit
773
+ - linear_offset: offsets the center of the tail or socket along the line by the ammount specified
774
+ - tolerance: the tolerance for the split
775
+ - tail_angle_offset: the adjustment pitch of angle of the dovetail (0 will result in a square dovetail)
776
+ - taper_angle: an extra shrinking factor for the dovetail size, allows for easier assembly
777
+ - length_ratio: the ratio of the length of the tongue to the total length of the dovetail
778
+ - depth_ratio: the ratio of the depth of the tongue to the total length of the dovetail
779
+ - scarf_angle: setting this to a non-zero value will tilt the dovetail along the Z axis which may improve part stability
780
+ - vertical_offset: offsets the dovetail along the Z axis by the ammount specified, which results in a straight line
781
+ cut on one side, and provides a hard stop for fitting. A positive number results in a straight cut on the bottom
782
+ of the part passed, a negagive number results in a straight cut on the top of the part passed
783
+ """
784
+ if start == end:
785
+ raise ValueError("start and end points cannot be the same")
786
+ if abs(vertical_offset) > part.bounding_box().size.Z:
787
+ raise ValueError("Vertical offset cannot be greater than the part's height")
788
+ if vertical_offset < 0 and taper_angle < 0:
789
+ raise ValueError(
790
+ "a negative taper_angle and a positive vertical_offset will result in an invalid dovetail"
791
+ )
792
+ if vertical_offset > 0 and taper_angle > 0:
793
+ raise ValueError(
794
+ "a positive taper_angle and a positive vertical_offset will result in an invalid dovetail"
795
+ )
796
+
797
+ max_dimension = (
798
+ max(
799
+ part.bounding_box().size.X,
800
+ part.bounding_box().size.Y,
801
+ part.bounding_box().size.Z,
802
+ )
803
+ * 3
804
+ )
805
+
806
+ vertical_tolerance_adjustment = (
807
+ vertical_tolerance
808
+ * (1 if section == DovetailPart.TAIL else -1)
809
+ * (1 if vertical_offset > 0 else -1)
810
+ )
811
+ scarf_offset = (part.bounding_box().size.Z) * tan(radians(scarf_angle)) / 2
812
+ vertical_scarf_offset = (
813
+ abs(vertical_offset) - vertical_tolerance_adjustment
814
+ ) * tan(radians(scarf_angle))
815
+
816
+ taper_offset = (
817
+ part.bounding_box().size.Z
818
+ - abs(vertical_offset)
819
+ - vertical_tolerance_adjustment
820
+ ) * tan(radians(abs(taper_angle)))
821
+ with BuildPart() as intersect:
822
+ if vertical_offset > 0:
823
+ add(
824
+ subpart_section(
825
+ start=start,
826
+ end=end,
827
+ max_dimension=max_dimension,
828
+ section=section,
829
+ style=style,
830
+ floor_z=part.bounding_box().min.Z,
831
+ floor_taper_distance=0, # fix taper_offset if (taper_angle < 0) else 0,
832
+ floor_scarf_offset=-scarf_offset,
833
+ top_z=part.bounding_box().min.Z
834
+ + vertical_offset
835
+ + vertical_tolerance_adjustment,
836
+ top_taper_distance=0, # fix
837
+ linear_offset=linear_offset,
838
+ top_scarf_offset=-scarf_offset + vertical_scarf_offset,
839
+ tolerance=tolerance,
840
+ slot_count=slot_count,
841
+ depth=depth,
842
+ straighten_dovetail=True,
843
+ )
844
+ )
845
+ current_floor = (
846
+ part.bounding_box().min.Z
847
+ if vertical_offset <= 0
848
+ else part.bounding_box().min.Z + abs(vertical_offset) + vertical_tolerance
849
+ )
850
+ add(
851
+ subpart_section(
852
+ start=start,
853
+ end=end,
854
+ max_dimension=max_dimension,
855
+ section=section,
856
+ style=style,
857
+ floor_z=current_floor,
858
+ floor_taper_distance=taper_offset if (taper_angle < 0) else 0,
859
+ floor_scarf_offset=(
860
+ -scarf_offset
861
+ if vertical_offset <= 0
862
+ else -scarf_offset + vertical_scarf_offset
863
+ ),
864
+ top_z=part.bounding_box().max.Z
865
+ + (
866
+ 0
867
+ if vertical_offset >= 0
868
+ else vertical_offset + vertical_tolerance_adjustment
869
+ ),
870
+ top_taper_distance=taper_offset,
871
+ top_scarf_offset=scarf_offset
872
+ - (0 if vertical_offset >= 0 else vertical_scarf_offset),
873
+ tolerance=tolerance,
874
+ slot_count=slot_count,
875
+ depth=depth,
876
+ linear_offset=linear_offset,
877
+ tail_angle_offset=tail_angle_offset,
878
+ length_ratio=length_ratio,
879
+ depth_ratio=depth_ratio,
880
+ straighten_dovetail=False,
881
+ )
882
+ )
883
+ if vertical_offset < 0:
884
+ current_floor = part.bounding_box().max.Z + (
885
+ 0
886
+ if vertical_offset >= 0
887
+ else vertical_offset + vertical_tolerance_adjustment
888
+ )
889
+ add(
890
+ subpart_section(
891
+ start=start,
892
+ end=end,
893
+ max_dimension=max_dimension,
894
+ section=section,
895
+ style=style,
896
+ floor_z=current_floor,
897
+ floor_taper_distance=0,
898
+ floor_scarf_offset=scarf_offset - vertical_scarf_offset,
899
+ top_z=part.bounding_box().max.Z,
900
+ top_taper_distance=0,
901
+ top_scarf_offset=scarf_offset,
902
+ tolerance=tolerance,
903
+ slot_count=slot_count,
904
+ depth=depth,
905
+ linear_offset=linear_offset,
906
+ tail_angle_offset=tail_angle_offset,
907
+ length_ratio=length_ratio,
908
+ depth_ratio=depth_ratio,
909
+ straighten_dovetail=True,
910
+ )
911
+ )
912
+ add(part, mode=Mode.INTERSECT)
913
+ if click_fit_radius != 0:
914
+ intersect.part = subpart_divots(
915
+ subpart=intersect.part,
916
+ start=start,
917
+ end=end,
918
+ section=section,
919
+ style=style,
920
+ tolerance=tolerance,
921
+ vertical_tolerance=vertical_tolerance,
922
+ scarf_angle=scarf_angle,
923
+ linear_offset=linear_offset,
924
+ taper_angle=taper_angle,
925
+ depth_ratio=depth_ratio,
926
+ length_ratio=length_ratio,
927
+ vertical_offset=vertical_offset,
928
+ click_fit_radius=click_fit_radius,
929
+ )
930
+
931
+ return intersect.part
932
+
933
+
934
+ def tslot_split_line(
935
+ start: Point,
936
+ end: Point,
937
+ section: DovetailPart = DovetailPart.TAIL,
938
+ slot_count: int = 1,
939
+ depth: float = 2,
940
+ tolerance: float = 0.1,
941
+ taper_distance=0,
942
+ ) -> Line:
943
+ """
944
+ given a start and end point, returns a tslot split line as a Line object
945
+ -------
946
+ arguments:
947
+ - start: the start point for the dovetail line
948
+ - end: the end point for the dovetail line
949
+ - section: the section of the dovetail to create (DovetailPart.TAIL or DovetailPart.SOCKET)
950
+ - linear_offset: offsets the center of the tail or socket along the line by the ammount specified
951
+ - tolerance: the tolerance for the split
952
+ - tail_angle_offset: the adjustment pitch of angle of the dovetail (0 will result in a square dovetail)
953
+ - taper_distance: an extra shrinking factor for the dovetail size, allows for easier assembly
954
+ - length_ratio: the ratio of the length of the tongue to the total length of the dovetail
955
+ - depth_ratio: the ratio of the depth of the tongue to the total length of the dovetail
956
+ """
957
+ base_angle = start.angle_to(end)
958
+ length = start.distance_to(end)
959
+ base_width = depth * 2
960
+ next_distance = (length - (base_width * slot_count)) / (slot_count + 1)
961
+ dovetail_tolerance = (
962
+ -(abs(tolerance / 2)) if section == DovetailPart.TAIL else abs(tolerance / 2)
963
+ )
964
+
965
+ adjusted_start_point = start.related_point(base_angle - 90, dovetail_tolerance)
966
+ adjusted_end_point = adjusted_start_point.related_point(base_angle, length)
967
+
968
+ last_point = adjusted_start_point
969
+
970
+ with BuildLine() as tslot_outline:
971
+
972
+ for slot_index in range(slot_count):
973
+ root_start = last_point.related_point(base_angle, next_distance)
974
+ trunk_start = root_start.related_point(
975
+ base_angle, depth / 2 + dovetail_tolerance + taper_distance
976
+ )
977
+ branch_start = trunk_start.related_point(
978
+ base_angle + 90, depth / 2 + dovetail_tolerance * 2 + taper_distance
979
+ )
980
+ mid_trunk_start = midpoint(trunk_start, branch_start)
981
+ branch_start_inner = branch_start.related_point(base_angle, -depth / 2)
982
+ branch_start_inner_mid = midpoint(branch_start_inner, branch_start)
983
+ branch_start_outer = branch_start_inner.related_point(
984
+ base_angle + 90, depth / 2 - dovetail_tolerance * 2 - taper_distance * 2
985
+ )
986
+ branch_start_mid = midpoint(branch_start_inner, branch_start_outer)
987
+ branch_end_inner = branch_start_inner.related_point(
988
+ base_angle, depth * 2 - dovetail_tolerance * 2 - taper_distance * 2
989
+ )
990
+ branch_end_outer = branch_start_outer.related_point(
991
+ base_angle, depth * 2 - dovetail_tolerance * 2 - taper_distance * 2
992
+ )
993
+ branch_mid = midpoint(branch_start_outer, branch_end_outer)
994
+ branch_end_mid = midpoint(branch_end_inner, branch_end_outer)
995
+ branch_end = branch_start.related_point(
996
+ base_angle, depth - dovetail_tolerance * 2 - taper_distance * 2
997
+ )
998
+ branch_end_inner_mid = midpoint(branch_end_inner, branch_end)
999
+ trunk_end = trunk_start.related_point(
1000
+ base_angle, depth - dovetail_tolerance * 2 - taper_distance * 2
1001
+ )
1002
+ mid_trunk_end = midpoint(trunk_end, branch_end)
1003
+
1004
+ root_end = root_start.related_point(base_angle, depth * 2)
1005
+ FilletPolyline(
1006
+ last_point,
1007
+ trunk_start,
1008
+ mid_trunk_start,
1009
+ radius=abs(tolerance) * (2 if section == DovetailPart.TAIL else 3),
1010
+ )
1011
+ FilletPolyline(
1012
+ mid_trunk_start,
1013
+ branch_start,
1014
+ branch_start_inner_mid,
1015
+ radius=abs(tolerance) * (2 if section == DovetailPart.TAIL else 3),
1016
+ )
1017
+ FilletPolyline(
1018
+ branch_start_inner_mid,
1019
+ branch_start_inner,
1020
+ branch_start_mid,
1021
+ radius=abs(tolerance) * (3 if section == DovetailPart.TAIL else 2),
1022
+ )
1023
+ FilletPolyline(
1024
+ branch_start_mid,
1025
+ branch_start_outer,
1026
+ branch_mid,
1027
+ radius=abs(tolerance) * (3 if section == DovetailPart.TAIL else 2),
1028
+ )
1029
+ FilletPolyline(
1030
+ branch_mid,
1031
+ branch_end_outer,
1032
+ branch_end_mid,
1033
+ radius=abs(tolerance) * (3 if section == DovetailPart.TAIL else 2),
1034
+ )
1035
+ FilletPolyline(
1036
+ branch_end_mid,
1037
+ branch_end_inner,
1038
+ branch_end_inner_mid,
1039
+ radius=abs(tolerance) * (3 if section == DovetailPart.TAIL else 2),
1040
+ )
1041
+ FilletPolyline(
1042
+ branch_end_inner_mid,
1043
+ branch_end,
1044
+ mid_trunk_end,
1045
+ radius=abs(tolerance) * (2 if section == DovetailPart.TAIL else 3),
1046
+ )
1047
+ FilletPolyline(
1048
+ mid_trunk_end,
1049
+ trunk_end,
1050
+ root_end,
1051
+ radius=abs(tolerance) * (2 if section == DovetailPart.TAIL else 3),
1052
+ )
1053
+ last_point = root_end
1054
+ Polyline(last_point, adjusted_end_point)
1055
+ return tslot_outline.line
1056
+
1057
+
1058
+ def dovetail_split_line(
1059
+ start: Point,
1060
+ end: Point,
1061
+ section: DovetailPart = DovetailPart.TAIL,
1062
+ linear_offset: float = 0,
1063
+ tolerance: float = 0.025,
1064
+ tail_angle_offset: float = 15,
1065
+ taper_distance: float = 0,
1066
+ length_ratio: float = 1 / 3,
1067
+ depth_ratio: float = 1 / 6,
1068
+ ) -> Line:
1069
+ """
1070
+ given a start and end point, returns a dovetail split line as a Line object
1071
+ -------
1072
+ arguments:
1073
+ - start: the start point for the dovetail line
1074
+ - end: the end point for the dovetail line
1075
+ - section: the section of the dovetail to create (DovetailPart.TAIL or DovetailPart.SOCKET)
1076
+ - linear_offset: offsets the center of the tail or socket along the line by the ammount specified
1077
+ - tolerance: the tolerance for the split
1078
+ - tail_angle_offset: the adjustment pitch of angle of the dovetail (0 will result in a square dovetail)
1079
+ - taper_distance: an extra shrinking factor for the dovetail size, allows for easier assembly
1080
+ - length_ratio: the ratio of the length of the tongue to the total length of the dovetail
1081
+ - depth_ratio: the ratio of the depth of the tongue to the total length of the dovetail
1082
+ """
1083
+ dovetail_tolerance = (
1084
+ -(abs(tolerance / 2)) if section == DovetailPart.TAIL else abs(tolerance / 2)
1085
+ )
1086
+
1087
+ base_angle = start.angle_to(end)
1088
+ tail_angle_tolerance_adjustment = dovetail_tolerance * tan(
1089
+ radians(tail_angle_offset)
1090
+ )
1091
+ base_angle = start.angle_to(end)
1092
+ length = start.distance_to(end)
1093
+ tongue_length = length * length_ratio + (dovetail_tolerance * 2)
1094
+ tongue_depth = length * depth_ratio
1095
+ tail_angle_extension = (tongue_depth) * tan(radians(tail_angle_offset))
1096
+ adjusted_start_point = start.related_point(base_angle - 90, dovetail_tolerance)
1097
+
1098
+ tail_end_start = adjusted_start_point.related_point(
1099
+ base_angle,
1100
+ length / 2
1101
+ - tongue_length / 2
1102
+ - tail_angle_tolerance_adjustment
1103
+ + linear_offset
1104
+ + taper_distance,
1105
+ ).related_point(base_angle - 90, tongue_depth - taper_distance / 2)
1106
+
1107
+ tail_end = adjusted_start_point.related_point(
1108
+ base_angle,
1109
+ length / 2
1110
+ + tongue_length / 2
1111
+ + tail_angle_tolerance_adjustment
1112
+ + linear_offset
1113
+ - taper_distance,
1114
+ ).related_point(base_angle - 90, tongue_depth - taper_distance / 2)
1115
+
1116
+ tail_base_start = adjusted_start_point.related_point(
1117
+ base_angle,
1118
+ length / 2
1119
+ - tongue_length / 2
1120
+ + tail_angle_extension
1121
+ - tail_angle_tolerance_adjustment
1122
+ + linear_offset
1123
+ + taper_distance / 2,
1124
+ )
1125
+
1126
+ tail_base_resume = adjusted_start_point.related_point(
1127
+ base_angle,
1128
+ length / 2
1129
+ + tongue_length / 2
1130
+ - tail_angle_extension
1131
+ + tail_angle_tolerance_adjustment
1132
+ + linear_offset
1133
+ - taper_distance / 2,
1134
+ )
1135
+
1136
+ adjusted_end_point = adjusted_start_point.related_point(base_angle, length)
1137
+
1138
+ with BuildLine() as dovetail_outline:
1139
+ FilletPolyline(
1140
+ adjusted_start_point,
1141
+ tail_base_start,
1142
+ midpoint(tail_base_start, tail_end_start),
1143
+ radius=abs(dovetail_tolerance) * (2 if section == DovetailPart.TAIL else 3),
1144
+ )
1145
+ FilletPolyline(
1146
+ midpoint(tail_base_start, tail_end_start),
1147
+ tail_end_start,
1148
+ midpoint(tail_end_start, tail_end),
1149
+ radius=abs(dovetail_tolerance) * (3 if section == DovetailPart.TAIL else 2),
1150
+ )
1151
+ FilletPolyline(
1152
+ midpoint(tail_end_start, tail_end),
1153
+ tail_end,
1154
+ midpoint(tail_end, tail_base_resume),
1155
+ radius=abs(dovetail_tolerance) * (3 if section == DovetailPart.TAIL else 2),
1156
+ )
1157
+ FilletPolyline(
1158
+ midpoint(tail_end, tail_base_resume),
1159
+ tail_base_resume,
1160
+ adjusted_end_point,
1161
+ radius=abs(dovetail_tolerance) * (2 if section == DovetailPart.TAIL else 3),
1162
+ )
1163
+
1164
+ return dovetail_outline.line
1165
+
1166
+
1167
+ # if __name__ == "__main__":
1168
+ # from ocp_vscode import show, Camera
1169
+ # with BuildPart(mode=Mode.PRIVATE) as test:
1170
+ # Box(40, 200, 78.7, align=(Align.CENTER, Align.CENTER, Align.MIN))
1171
+
1172
+ # splines = snugtail_subpart_outline(
1173
+ # test.part,
1174
+ # Point(-20, 0),
1175
+ # Point(20, 0),
1176
+ # section=DovetailPart.SOCKET,
1177
+ # taper_distance=0,
1178
+ # tolerance=0.8,
1179
+ # length_ratio=.6,
1180
+ # depth_ratio=.3,
1181
+ # tail_angle_offset=35,
1182
+ # # scarf_angle=20,
1183
+ # # straighten_dovetail=True,
1184
+ # )
1185
+ # spline = snugtail_subpart_outline(
1186
+ # test.part,
1187
+ # Point(-20, 0),
1188
+ # Point(20, 0),
1189
+ # section=DovetailPart.TAIL,
1190
+ # taper_distance=4,
1191
+ # tolerance=0.8,
1192
+ # length_ratio=.6,
1193
+ # depth_ratio=.3,
1194
+ # tail_angle_offset=35,
1195
+ # # scarf_angle=20,
1196
+ # # straighten_dovetail=True,
1197
+ # )
1198
+ # splines.color = (0.5, 0.5, 0.5)
1199
+
1200
+ # show(spline, splines, reset_camera=Camera.KEEP)
1201
+
1202
+ if __name__ == "__main__":
1203
+ from ocp_vscode import show, Camera
1204
+
1205
+ with BuildPart(mode=Mode.PRIVATE) as test:
1206
+ Box(40, 200, 78.7, align=(Align.CENTER, Align.CENTER, Align.MIN))
1207
+ with BuildPart(
1208
+ Plane.XY.offset(73.6),
1209
+ mode=Mode.SUBTRACT,
1210
+ ):
1211
+ Cylinder(
1212
+ 25,
1213
+ 200,
1214
+ rotation=(90, 0, 0),
1215
+ )
1216
+
1217
+ tl = dovetail_subpart(
1218
+ test.part,
1219
+ Point(-20, 0),
1220
+ Point(20, 0),
1221
+ section=DovetailPart.TAIL,
1222
+ style=DovetailStyle.T_SLOT,
1223
+ tolerance=0.1,
1224
+ depth=2,
1225
+ slot_count=1,
1226
+ taper_angle=0.25,
1227
+ scarf_angle=2,
1228
+ vertical_offset=-14.33333,
1229
+ tail_angle_offset=25,
1230
+ length_ratio=0.7,
1231
+ depth_ratio=0.3,
1232
+ ).move(Location((0, 0, 0)))
1233
+ sckt = dovetail_subpart(
1234
+ test.part,
1235
+ Point(-20, 0),
1236
+ Point(20, 0),
1237
+ section=DovetailPart.SOCKET,
1238
+ style=DovetailStyle.T_SLOT,
1239
+ tolerance=0.1,
1240
+ depth=2,
1241
+ slot_count=1,
1242
+ taper_angle=0.25,
1243
+ scarf_angle=2,
1244
+ vertical_offset=-14.33333,
1245
+ tail_angle_offset=25,
1246
+ length_ratio=0.7,
1247
+ depth_ratio=0.3,
1248
+ )
1249
+ sckt.color = (0.5, 0.5, 0.5)
1250
+ splines = dovetail_subpart_outline(
1251
+ Point(-20, 0),
1252
+ Point(20, 0),
1253
+ section=DovetailPart.SOCKET,
1254
+ style=DovetailStyle.T_SLOT,
1255
+ taper_distance=0,
1256
+ tolerance=0.1,
1257
+ length_ratio=0.6,
1258
+ depth_ratio=0.3,
1259
+ tail_angle_offset=35,
1260
+ slot_count=1,
1261
+ depth=2,
1262
+ # scarf_angle=20,
1263
+ # straighten_dovetail=True,
1264
+ )
1265
+ spline = dovetail_subpart_outline(
1266
+ Point(-20, 0),
1267
+ Point(20, 0),
1268
+ section=DovetailPart.TAIL,
1269
+ style=DovetailStyle.T_SLOT,
1270
+ taper_distance=0,
1271
+ tolerance=0.1,
1272
+ length_ratio=0.6,
1273
+ depth_ratio=0.3,
1274
+ tail_angle_offset=35,
1275
+ slot_count=1,
1276
+ depth=2,
1277
+ # scarf_angle=20,
1278
+ # straighten_dovetail=True,
1279
+ )
1280
+ # spline = snugtail_subpart_outline(
1281
+ # Point(-20, 0),
1282
+ # Point(20, 0),
1283
+ # section=DovetailPart.TAIL,
1284
+ # taper_distance=0,
1285
+ # tolerance=0.8,
1286
+ # length_ratio=0.6,
1287
+ # depth_ratio=0.3,
1288
+ # tail_angle_offset=35,
1289
+ # # scarf_angle=20,
1290
+ # # straighten_dovetail=True,
1291
+ # )
1292
+ # splines = dovetail_subpart_outline(
1293
+ # test.part,
1294
+ # Point(-20, 0),
1295
+ # Point(20, 0),
1296
+ # section=DovetailPart.SOCKET,
1297
+ # taper_distance=0,
1298
+ # tolerance=0.8,
1299
+ # length_ratio=.6,
1300
+ # depth_ratio=.3,
1301
+ # tail_angle_offset=35,
1302
+ # # scarf_angle=20,
1303
+ # # straighten_dovetail=True,
1304
+ # )
1305
+ # spline = dovetail_subpart_outline(
1306
+ # test.part,
1307
+ # Point(-20, 0),
1308
+ # Point(20, 0),
1309
+ # section=DovetailPart.TAIL,
1310
+ # taper_distance=0,
1311
+ # tolerance=0.8,
1312
+ # length_ratio=.6,
1313
+ # depth_ratio=.3,
1314
+ # tail_angle_offset=35,
1315
+ # # scarf_angle=20,
1316
+ # # straighten_dovetail=True,
1317
+ # )
1318
+ splines.color = (0.5, 0.5, 0.5)
1319
+ with BuildSketch() as sks:
1320
+ add(splines)
1321
+ make_face()
1322
+ sks.color = (0.5, 0.5, 0.5)
1323
+ with BuildSketch() as sk:
1324
+ add(spline)
1325
+ make_face()
1326
+ from build123d import export_stl
1327
+
1328
+ show(
1329
+ tl,
1330
+ sckt,
1331
+ # sk,
1332
+ # sks,
1333
+ # spline,
1334
+ # splines,
1335
+ reset_camera=Camera.KEEP,
1336
+ )
1337
+ # export_stl(tl, "tail.stl")
1338
+ # export_stl(sckt, "socket.stl")