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/hexwall.py ADDED
@@ -0,0 +1,102 @@
1
+ """
2
+ HexWall
3
+
4
+ name: hexwall.py
5
+ by: x0pherl
6
+ date: Jan 19th 2025
7
+
8
+ desc:
9
+ This build123d python module creates a hexagonal pattern within a box, resulting
10
+ in a grid of hexagons.
11
+ """
12
+
13
+ from math import sqrt
14
+ from typing import Union
15
+
16
+ from build123d import (
17
+ Align,
18
+ Axis,
19
+ BasePartObject,
20
+ Box,
21
+ BuildPart,
22
+ BuildSketch,
23
+ HexLocations,
24
+ Mode,
25
+ Part,
26
+ RegularPolygon,
27
+ RotationLike,
28
+ extrude,
29
+ tuplify,
30
+ )
31
+ from ocp_vscode import Camera, show
32
+
33
+
34
+ class HexWall(BasePartObject):
35
+
36
+ def __init__(
37
+ self,
38
+ length: float,
39
+ width: float,
40
+ height: float,
41
+ apothem: float,
42
+ wall_thickness: float,
43
+ inverse=False,
44
+ rotation: RotationLike = (0, 0, 0),
45
+ align: Union[None, Align, tuple[Align, Align, Align]] = None,
46
+ mode: Mode = Mode.ADD,
47
+ ):
48
+ """
49
+ Part Object: hexwall
50
+ -------
51
+ arguments:
52
+ - length (float): box size
53
+ - width (float): box size
54
+ - height (float): box size
55
+ - apothem (float): the distance between two paralel edges of the hexagon
56
+ - rotation (RotationLike, optional): angles to rotate about axes. Defaults to (0, 0, 0)
57
+ - align (Align | tuple[Align, Align, Align] | None, optional): align MIN, CENTER,
58
+ or MAX of object. Defaults to (Align.CENTER, Align.CENTER, Align.CENTER)
59
+ - mode (Mode, optional): combine mode. Defaults to Mode.ADD
60
+ """
61
+ with BuildPart() as wall:
62
+ hexwall_radius = 2 * sqrt(3) / 3 * apothem
63
+ hexwall_xcount = int(length // ((sqrt(3) / 2 * apothem) / 2)) + 2
64
+ if hexwall_xcount % 2 == 0:
65
+ hexwall_xcount += 1
66
+ Box(length=length, width=width, height=height, align=tuplify(align, 3))
67
+ combine_mode = Mode.INTERSECT if inverse else Mode.SUBTRACT
68
+ with BuildPart(mode=combine_mode):
69
+ with BuildSketch(wall.faces().sort_by(Axis.Z)[0]) as sk:
70
+ with HexLocations(
71
+ radius=hexwall_radius,
72
+ x_count=hexwall_xcount,
73
+ y_count=int(width // apothem / 2) + 2,
74
+ align=(Align.CENTER, Align.CENTER),
75
+ ):
76
+ RegularPolygon(
77
+ radius=2 * sqrt(3) / 3 * (apothem - wall_thickness / 2),
78
+ major_radius=False,
79
+ side_count=6,
80
+ )
81
+ extrude(sk.sketch, -height)
82
+ part = wall.part
83
+ part.label = "hexwall"
84
+
85
+ super().__init__(
86
+ part=part, rotation=rotation, align=tuplify(align, 3), mode=mode
87
+ )
88
+
89
+
90
+ if __name__ == "__main__":
91
+ show(
92
+ HexWall(
93
+ width=200,
94
+ length=400,
95
+ height=2,
96
+ apothem=10,
97
+ wall_thickness=2,
98
+ inverse=True,
99
+ align=(Align.CENTER, Align.CENTER, Align.MIN),
100
+ ),
101
+ reset_camera=Camera.KEEP,
102
+ )
@@ -0,0 +1,504 @@
1
+ from build123d import (
2
+ Align,
3
+ Axis,
4
+ Box,
5
+ BuildLine,
6
+ BuildPart,
7
+ BuildSketch,
8
+ Compound,
9
+ GridLocations,
10
+ Location,
11
+ Part,
12
+ PolarLocations,
13
+ Polyline,
14
+ Mode,
15
+ add,
16
+ extrude,
17
+ fillet,
18
+ make_face,
19
+ pack,
20
+ )
21
+ from dataclasses import field
22
+ from b3dkit.point import Point, midpoint
23
+ from b3dkit.basic_shapes import (
24
+ DiamondCylinder,
25
+ opposite_length,
26
+ )
27
+ from b3dkit.click_fit import Divot
28
+
29
+ from ocp_vscode import show, Camera
30
+
31
+
32
+ def _slide_top_rail_cut(
33
+ part_width: float,
34
+ part_depth: float,
35
+ rail_height: float,
36
+ wall_thickness: float,
37
+ rail_angle: float = 0,
38
+ effective_tolerance: float = 0.05,
39
+ ):
40
+ """
41
+ Creates the rail cut geometry for the sliding mechanism of a high top slide box.
42
+
43
+ This internal function generates the precise cut pattern needed for the sliding rails,
44
+ including diamond-shaped guides and tolerance adjustments. The rails allow smooth
45
+ sliding motion while maintaining secure positioning.
46
+
47
+ args:
48
+ - part_width: the width of the base part in millimeters
49
+ - part_depth: the depth of the base part in millimeters
50
+ - rail_height: the height of the rail system in millimeters
51
+ - wall_thickness: the thickness of the box walls in millimeters
52
+ - rail_angle: the angle of the rails in degrees for improved sliding
53
+ - effective_tolerance: the calculated tolerance for the sliding fit
54
+ """
55
+ rail_front_right = Point(
56
+ (part_width - wall_thickness * 2 / 3 + effective_tolerance) / 2,
57
+ (part_depth - wall_thickness) / 2,
58
+ )
59
+ rail_back_right = Point(
60
+ (
61
+ rail_front_right.x
62
+ - (
63
+ 0
64
+ if (rail_angle == 0)
65
+ else opposite_length(
66
+ rail_angle, adjacent_length=part_depth - wall_thickness
67
+ )
68
+ )
69
+ ),
70
+ -(part_depth - wall_thickness) / 2,
71
+ )
72
+
73
+ with BuildPart(
74
+ Location((0, wall_thickness / 2, 0)),
75
+ mode=Mode.SUBTRACT,
76
+ ) as rail_cut:
77
+ with BuildSketch(Location((0, wall_thickness / 2))):
78
+ with BuildLine():
79
+
80
+ Polyline(
81
+ rail_front_right,
82
+ rail_back_right,
83
+ Point(-(rail_back_right.x), rail_back_right.y),
84
+ Point(-(rail_front_right.x), rail_front_right.y),
85
+ rail_front_right,
86
+ )
87
+ make_face()
88
+ extrude(amount=rail_height)
89
+ guide_radius = wall_thickness / 3 - effective_tolerance / 2
90
+ with BuildPart(
91
+ (
92
+ Location(
93
+ (
94
+ rail_front_right.x,
95
+ rail_front_right.y + wall_thickness / 2,
96
+ rail_height,
97
+ )
98
+ )
99
+ ),
100
+ mode=Mode.ADD,
101
+ ):
102
+ DiamondCylinder(
103
+ radius=wall_thickness - effective_tolerance / 2,
104
+ height=part_depth - wall_thickness,
105
+ align=(Align.MAX, Align.CENTER, Align.MIN),
106
+ rotation=(90, 0, -rail_angle),
107
+ )
108
+ with BuildPart(
109
+ (
110
+ Location(
111
+ (
112
+ -rail_front_right.x,
113
+ rail_front_right.y + wall_thickness / 2,
114
+ rail_height,
115
+ )
116
+ )
117
+ ),
118
+ mode=Mode.ADD,
119
+ ):
120
+ DiamondCylinder(
121
+ radius=wall_thickness - effective_tolerance / 2,
122
+ height=part_depth - wall_thickness,
123
+ align=(Align.MIN, Align.CENTER, Align.MIN),
124
+ rotation=(90, 0, rail_angle),
125
+ )
126
+
127
+ with BuildPart(
128
+ (
129
+ Location(
130
+ (
131
+ rail_front_right.x,
132
+ rail_front_right.y + wall_thickness / 2,
133
+ rail_height / 2 - effective_tolerance,
134
+ )
135
+ )
136
+ ),
137
+ mode=Mode.SUBTRACT,
138
+ ):
139
+ DiamondCylinder(
140
+ radius=guide_radius,
141
+ height=part_depth,
142
+ align=(Align.CENTER, Align.CENTER, Align.MIN),
143
+ rotation=(90, 0, -rail_angle),
144
+ )
145
+ with BuildPart(
146
+ (
147
+ Location(
148
+ (
149
+ -rail_front_right.x,
150
+ rail_front_right.y + wall_thickness / 2,
151
+ rail_height / 2 - effective_tolerance,
152
+ )
153
+ )
154
+ ),
155
+ mode=Mode.SUBTRACT,
156
+ ):
157
+ DiamondCylinder(
158
+ radius=guide_radius,
159
+ height=part_depth,
160
+ align=(Align.CENTER, Align.CENTER, Align.MIN),
161
+ rotation=(90, 0, rail_angle),
162
+ )
163
+
164
+ return rail_cut.part
165
+
166
+
167
+ def _high_top_slide_box_top(
168
+ base_part: Part,
169
+ top_height: float,
170
+ rail_height: float,
171
+ wall_thickness: float,
172
+ rail_angle: float = 0,
173
+ divot_radius: float = 0.5,
174
+ thumb_radius: float = 0,
175
+ tolerance: float = 0.2,
176
+ cut_template: bool = False,
177
+ ) -> Part:
178
+ """
179
+ Creates the top component for a high top slide box with rails and optional divots.
180
+
181
+ This internal function generates either the actual sliding lid or a template for cutting
182
+ the base, depending on the cut_template parameter. It includes rail geometry, divots for
183
+ positioning feedback, and proper tolerance adjustments.
184
+
185
+ args:
186
+ - base_part: the base part that defines the outer dimensions
187
+ - top_height: the height of the sliding top portion in millimeters
188
+ - rail_height: the height of the rail system in millimeters
189
+ - wall_thickness: the thickness of the box walls in millimeters
190
+ - rail_angle: the angle of the rails in degrees for smoother sliding
191
+ - divot_radius: the radius of positioning divots, set to 0 to disable
192
+ - thumb_radius: the radius for thumb grips (currently unused)
193
+ - tolerance: the clearance between moving parts in millimeters
194
+ - cut_template: whether this is for cutting (True) or building the lid (False)
195
+ """
196
+ effective_tolerance = abs(tolerance) / (-2 if cut_template else 2)
197
+ with BuildPart() as top:
198
+ part_width = base_part.bounding_box().size.X
199
+ part_depth = base_part.bounding_box().size.Y
200
+ part_height = base_part.bounding_box().size.Z
201
+ part_max_dimension = max(part_width, part_depth, part_height)
202
+ with BuildPart(
203
+ Location(
204
+ (
205
+ 0,
206
+ 0,
207
+ -part_height
208
+ + top_height
209
+ + rail_height
210
+ + wall_thickness
211
+ - effective_tolerance * 4,
212
+ )
213
+ )
214
+ ):
215
+ add(base_part)
216
+ with BuildPart(mode=Mode.INTERSECT):
217
+ Box(
218
+ part_max_dimension,
219
+ part_max_dimension,
220
+ part_max_dimension,
221
+ align=(Align.CENTER, Align.CENTER, Align.MIN),
222
+ )
223
+ with BuildPart(
224
+ Location((0, wall_thickness / 2, rail_height)),
225
+ mode=Mode.SUBTRACT,
226
+ ) as top_cut:
227
+ Box(
228
+ part_width - wall_thickness * 2 + effective_tolerance * 2,
229
+ part_depth - wall_thickness,
230
+ top_height,
231
+ align=(Align.CENTER, Align.CENTER, Align.MIN),
232
+ )
233
+
234
+ with BuildPart(
235
+ Location((0, 0, 0)),
236
+ mode=Mode.SUBTRACT,
237
+ ) as rail_cut:
238
+ add(
239
+ _slide_top_rail_cut(
240
+ part_width=part_width,
241
+ part_depth=part_depth,
242
+ rail_height=rail_height,
243
+ wall_thickness=wall_thickness,
244
+ rail_angle=rail_angle,
245
+ effective_tolerance=effective_tolerance,
246
+ )
247
+ )
248
+ if divot_radius > 0:
249
+ with BuildPart(
250
+ Location(
251
+ (0, (part_depth - wall_thickness) / 2, top_height + rail_height),
252
+ (180, 0, 0),
253
+ ),
254
+ ):
255
+ with PolarLocations(radius=part_width / 3 - wall_thickness, count=2):
256
+ Divot(
257
+ radius=divot_radius,
258
+ positive=not cut_template,
259
+ extend_base=True,
260
+ )
261
+
262
+ with BuildPart(
263
+ Location(
264
+ (0, (-part_depth + wall_thickness) / 2, 0),
265
+ (180, 0, 0),
266
+ ),
267
+ ):
268
+ with PolarLocations(radius=part_width / 3 - wall_thickness, count=2):
269
+ Divot(
270
+ radius=divot_radius,
271
+ positive=not cut_template,
272
+ extend_base=True,
273
+ )
274
+ top.part.label = "lid"
275
+ return top.part
276
+
277
+
278
+ def high_top_slide_box_lid(
279
+ base_part: Part,
280
+ top_height: float,
281
+ rail_height: float,
282
+ wall_thickness: float,
283
+ rail_angle: float = 0,
284
+ divot_radius: float = 0.5,
285
+ thumb_radius: float = 0,
286
+ tolerance: float = 0.2,
287
+ ) -> Part:
288
+ """
289
+ Creates the sliding lid component for a high top slide box.
290
+
291
+ The lid features rails that slide smoothly into the base component, with optional
292
+ divots for tactile positioning feedback. The lid maintains the top portion of the
293
+ original part while adding the necessary sliding mechanism.
294
+
295
+ args:
296
+ - base_part: the base part that defines the outer dimensions
297
+ - top_height: the height of the sliding top portion in millimeters
298
+ - rail_height: the height of the rail system in millimeters
299
+ - wall_thickness: the thickness of the box walls in millimeters
300
+ - rail_angle: the angle of the rails in degrees for smoother sliding
301
+ - divot_radius: the radius of positioning divots, set to 0 to disable
302
+ - thumb_radius: the radius for thumb grips (currently unused)
303
+ - tolerance: the clearance between moving parts in millimeters
304
+ """
305
+ lid = _high_top_slide_box_top(
306
+ base_part,
307
+ top_height,
308
+ rail_height,
309
+ wall_thickness,
310
+ rail_angle,
311
+ divot_radius,
312
+ thumb_radius,
313
+ tolerance,
314
+ cut_template=False,
315
+ )
316
+ lid.label = "box top"
317
+ return lid
318
+
319
+
320
+ def high_top_slide_box_base(
321
+ base_part: Part,
322
+ top_height: float,
323
+ rail_height: float,
324
+ wall_thickness: float,
325
+ rail_angle: float = 0,
326
+ divot_radius: float = 0.5,
327
+ thumb_radius: float = 0,
328
+ tolerance: float = 0.2,
329
+ ) -> Part:
330
+ """
331
+ Creates the base component for a high top slide box with rail channels.
332
+
333
+ The base is hollowed out to create storage space and includes channels that receive
334
+ the sliding lid's rails. Diamond-shaped guide cylinders help ensure smooth operation
335
+ and proper alignment of the sliding mechanism.
336
+
337
+ args:
338
+ - base_part: the base part that defines the outer dimensions
339
+ - top_height: the height of the sliding top portion in millimeters
340
+ - rail_height: the height of the rail system in millimeters
341
+ - wall_thickness: the thickness of the box walls in millimeters
342
+ - rail_angle: the angle of the rails in degrees for smoother sliding
343
+ - divot_radius: the radius of positioning divots, set to 0 to disable
344
+ - thumb_radius: the radius for thumb grips (currently unused)
345
+ - tolerance: the clearance between moving parts in millimeters
346
+ """
347
+ part_width = base_part.bounding_box().size.X
348
+ part_depth = base_part.bounding_box().size.Y
349
+ part_height = base_part.bounding_box().size.Z
350
+
351
+ with BuildPart() as boxbottom:
352
+ add(base_part)
353
+ with BuildPart(
354
+ Location(
355
+ (
356
+ 0,
357
+ 0,
358
+ wall_thickness,
359
+ ),
360
+ (0, 0, 0),
361
+ ),
362
+ mode=Mode.SUBTRACT,
363
+ ):
364
+ Box(
365
+ part_width - wall_thickness * 2,
366
+ part_depth - wall_thickness * 2,
367
+ part_height - wall_thickness,
368
+ align=(Align.CENTER, Align.CENTER, Align.MIN),
369
+ )
370
+ with BuildPart(
371
+ Location(
372
+ (
373
+ 0,
374
+ 0,
375
+ part_height
376
+ - top_height
377
+ - rail_height
378
+ - wall_thickness
379
+ + tolerance * 2,
380
+ ),
381
+ (0, 0, 0),
382
+ ),
383
+ mode=Mode.SUBTRACT,
384
+ ):
385
+ add(
386
+ _high_top_slide_box_top(
387
+ base_part,
388
+ top_height,
389
+ rail_height,
390
+ wall_thickness,
391
+ rail_angle,
392
+ divot_radius,
393
+ thumb_radius,
394
+ tolerance,
395
+ cut_template=True,
396
+ )
397
+ )
398
+ with BuildPart(
399
+ Location(
400
+ (
401
+ 0,
402
+ -part_depth / 2 + wall_thickness,
403
+ part_height
404
+ - top_height
405
+ - rail_height
406
+ - wall_thickness
407
+ + tolerance * 2,
408
+ )
409
+ ),
410
+ mode=Mode.SUBTRACT,
411
+ ):
412
+ with GridLocations(part_width, 0, 2, 1) as grid_locs:
413
+ DiamondCylinder(
414
+ radius=wall_thickness,
415
+ height=part_height * 2,
416
+ align=(Align.CENTER, Align.CENTER, Align.MIN),
417
+ )
418
+
419
+ boxbottom.part.label = "box bottom"
420
+ return boxbottom.part
421
+
422
+
423
+ def high_top_slide_box(
424
+ base_part: Part,
425
+ top_height: float,
426
+ rail_height: float,
427
+ wall_thickness: float,
428
+ rail_angle: float = 0,
429
+ divot_radius: float = 0.5,
430
+ thumb_radius: float = 0,
431
+ tolerance: float = 0.2,
432
+ ) -> Compound:
433
+ """
434
+ Creates a complete high top slide box with both lid and base components.
435
+
436
+ This function generates a sliding box system where the lid has significant height
437
+ and slides on rails into the base. The system uses diamond-shaped rails for smooth
438
+ operation and includes optional divots for positioning feedback. The lid is automatically
439
+ oriented for 3D printing.
440
+
441
+ args:
442
+ - base_part: the base part that defines the outer dimensions of the box
443
+ - top_height: the height of the sliding top portion in millimeters
444
+ - rail_height: the height of the rail system that guides sliding motion
445
+ - wall_thickness: the thickness of the box walls in millimeters
446
+ - rail_angle: the angle of the rails in degrees for smoother sliding
447
+ - divot_radius: the radius of positioning divots, set to 0 to disable
448
+ - thumb_radius: the radius for thumb grips (currently unused)
449
+ - tolerance: the clearance between moving parts in millimeters
450
+ """
451
+
452
+ return Compound(
453
+ label="slide box",
454
+ children=[
455
+ high_top_slide_box_lid(
456
+ base_part=base_part,
457
+ top_height=top_height,
458
+ rail_height=rail_height,
459
+ wall_thickness=wall_thickness,
460
+ rail_angle=rail_angle,
461
+ divot_radius=divot_radius,
462
+ thumb_radius=thumb_radius,
463
+ tolerance=tolerance,
464
+ ).rotate(Axis.X, 180),
465
+ high_top_slide_box_base(
466
+ base_part=base_part,
467
+ top_height=top_height,
468
+ rail_height=rail_height,
469
+ wall_thickness=wall_thickness,
470
+ rail_angle=rail_angle,
471
+ divot_radius=divot_radius,
472
+ thumb_radius=thumb_radius,
473
+ tolerance=tolerance,
474
+ ),
475
+ ],
476
+ )
477
+
478
+
479
+ if __name__ == "__main__":
480
+ with BuildPart() as base_box:
481
+ Box(44, 44, 44, align=(Align.CENTER, Align.CENTER, Align.MIN))
482
+ fillet(base_box.part.edges().filter_by(Axis.Z), radius=1.5)
483
+
484
+ sb = high_top_slide_box(
485
+ base_part=base_box.part,
486
+ top_height=5,
487
+ rail_height=10,
488
+ wall_thickness=4,
489
+ rail_angle=0.5,
490
+ divot_radius=0.5,
491
+ thumb_radius=0,
492
+ tolerance=0.1,
493
+ )
494
+ top = (
495
+ sb.children[0].move(Location((0, 0, 19)))
496
+ # .rotate(Axis.Z, 180)
497
+ # .rotate(Axis.Y, 180)
498
+ # .move(Location((-44 - 5, 44, sb.children[1].bounding_box().size.Z + 4 - 0.1)))
499
+ )
500
+ # show(sb, reset_camera=Camera.KEEP)
501
+ show(
502
+ pack([top, sb.children[1]], padding=5, align_z=True),
503
+ reset_camera=Camera.KEEP,
504
+ )