├── .gitattributes
├── .gitignore
├── Makefile
├── blender
├── alpakka.blend
├── anchor.blend
├── button_abxy.blend
├── button_dpad.blend
├── button_home.blend
├── button_select.blend
├── case_back.blend
├── case_cover.blend
├── case_front.blend
├── hexagon.blend
├── lock.blend
├── scrollwheel.blend
├── shared.blend
├── soldering_stand.blend
├── thumbstick.blend
├── trigger_R1.blend
├── trigger_R2.blend
└── trigger_R4.blend
├── build123d
├── button_abxy.py
├── button_dpad.py
├── button_select.py
├── common
│ └── vscode.py
├── cover.py
├── dongle_case.py
├── hexagon.py
├── thumbstick_right.py
├── trigger_r1.py
├── wheel.py
├── wheel_core.py
└── wheel_holder.py
├── contributor.md
├── license.md
├── preview
├── print_A.png
├── print_B.png
├── print_C.png
├── print_D.png
├── print_E.png
└── print_F.png
├── readme.md
└── scripts
├── export_b123d.py
└── export_blender.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.blend filter=lfs diff=lfs merge=lfs -text
2 | *.stl filter=lfs diff=lfs merge=lfs -text
3 | *.3mf filter=lfs diff=lfs merge=lfs -text
4 | *.png filter=lfs diff=lfs merge=lfs -text
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.blend1
2 | backup/
3 | stl/
4 | step/
5 | 3mf/
6 | release/
7 | blender/render/
8 | .vscode/
9 | __pycache__/
10 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-2.0-only
2 | # Copyright (C) 2022, Input Labs Oy.
3 |
4 | BLENDER := 'blender'
5 | ifeq ($(shell uname), Darwin)
6 | BLENDER := '/Applications/Blender.app/Contents/MacOS/Blender'
7 | endif
8 |
9 | default: release
10 |
11 | release: stl
12 | mkdir -p release/
13 | zip -u release/blender.zip blender/*.blend
14 | zip -u release/stl.zip stl/*.stl stl/**/*.stl
15 | zip -u release/step.zip step/*.step step/**/*.step
16 |
17 | clean:
18 | rm -rf release/*
19 | rm -rf stl/*
20 | rm -rf step/*
21 | mkdir -p stl
22 | mkdir -p step
23 | mkdir -p stl/variants
24 | mkdir -p step/variants
25 |
26 | stl: clean b123d blend
27 |
28 | blend:
29 | $(BLENDER) blender/case_front.blend --background --python scripts/export_blender.py
30 | $(BLENDER) blender/case_back.blend --background --python scripts/export_blender.py
31 | $(BLENDER) blender/trigger_R2.blend --background --python scripts/export_blender.py
32 | $(BLENDER) blender/trigger_R4.blend --background --python scripts/export_blender.py
33 | $(BLENDER) blender/anchor.blend --background --python scripts/export_blender.py
34 | $(BLENDER) blender/thumbstick.blend --background --python scripts/export_blender.py
35 | $(BLENDER) blender/button_home.blend --background --python scripts/export_blender.py
36 | $(BLENDER) blender/soldering_stand.blend --background --python scripts/export_blender.py
37 | # Variants.
38 | mv stl/007mm_thumbstick_L_loose.stl stl/007mm_thumbstick_L.stl
39 | mv stl/007mm_thumbstick_L_tight.stl stl/variants/007mm_thumbstick_L_tight.stl
40 |
41 | b123d:
42 | python3 scripts/export_b123d.py
43 |
--------------------------------------------------------------------------------
/blender/alpakka.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:60802ac69b196345199d52f88587afee7013258d80400d9cd11b053f87f35bd3
3 | size 183490
4 |
--------------------------------------------------------------------------------
/blender/anchor.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:05ddcc8c7b178ded1481b9ccd1069d311e1d945bf5efc7b746c6a9f273a06ab6
3 | size 183644
4 |
--------------------------------------------------------------------------------
/blender/button_abxy.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:c5568ec6a325b8f737813362a75b87b6939a06a7dabdbd7a1f7bfa8c0f5eb658
3 | size 221209
4 |
--------------------------------------------------------------------------------
/blender/button_dpad.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:6c1d532ebc054553e5c12afea5656464618be636b6bea6672972d8cf6ae8f56c
3 | size 285915
4 |
--------------------------------------------------------------------------------
/blender/button_home.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:ae2c6d3d3b3f3d5d5d172b8c29662d464fae68099518f7a18358f7957c961d32
3 | size 178419
4 |
--------------------------------------------------------------------------------
/blender/button_select.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:983e913eb667c241ea8b377b8e0745ba1faf7ead5a1ea81e39db7c2debcb6eb8
3 | size 283236
4 |
--------------------------------------------------------------------------------
/blender/case_back.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:0b33a23dd0c8d69be2427a536e8d4eda2d4d1384e4b2ac78995c45f111d59e4b
3 | size 212919
4 |
--------------------------------------------------------------------------------
/blender/case_cover.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:6415132df6d1757ca61ef205af2a2da70c1fa49bd03cdb25a6fc0e0f4706718a
3 | size 186754
4 |
--------------------------------------------------------------------------------
/blender/case_front.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:7041b2529e03b3514d17a8624f06c744d59d1a00a9b991d0dcc6741e22f1e8af
3 | size 299950
4 |
--------------------------------------------------------------------------------
/blender/hexagon.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:3866ebd7df47c8e50d0d3e954517cb699c4d1cd6c40fd815caba7ab85473faef
3 | size 220061
4 |
--------------------------------------------------------------------------------
/blender/lock.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:617fe97e1cb73675b447c859cd8f6f68e6270324628fff5a4d17cf870d0288ed
3 | size 186963
4 |
--------------------------------------------------------------------------------
/blender/scrollwheel.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:94c31dfce58b2dff5d6adf33e70aadd5cefbebdcfb692ac451d6979d69ac8981
3 | size 231911
4 |
--------------------------------------------------------------------------------
/blender/shared.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:3d21bed3ca48c5e93e5622233cc313ae49155a2c86a488f6649d842bf297ebfc
3 | size 175156
4 |
--------------------------------------------------------------------------------
/blender/soldering_stand.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:0a90ac27b7105003857f7562a0b1cdcf3e27076f2a2e178ae6948d0304497b72
3 | size 184668
4 |
--------------------------------------------------------------------------------
/blender/thumbstick.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:7cfa150a1049bf01ed500492a88e75020a154626c301cac0f8d3a6674d3d4516
3 | size 484845
4 |
--------------------------------------------------------------------------------
/blender/trigger_R1.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:377b69c276b28d05fc8129187909b6999e682d2da54f846addf383f8571072a6
3 | size 189872
4 |
--------------------------------------------------------------------------------
/blender/trigger_R2.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:6da2979908579a1cab58d352b608104cb7eded53361f4f01a6690969cd289469
3 | size 185674
4 |
--------------------------------------------------------------------------------
/blender/trigger_R4.blend:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:f88aada8e263c46c784e51917368ae12b7b4d3592a4af757bd6748a80ad45361
3 | size 192959
4 |
--------------------------------------------------------------------------------
/build123d/button_abxy.py:
--------------------------------------------------------------------------------
1 | from build123d import (
2 | BuildPart, BuildSketch, Rectangle, Circle, Mode, Plane, Axis,
3 | chamfer, extrude, loft)
4 |
5 |
6 | TOTAL_BUTTON_HEIGHT = 18.3
7 |
8 | LOWER_BUTTON_HEIGHT = 2.4
9 | LOWER_BUTTON_RAD = 5.8
10 | LOWER_BUTTON_WIDTH = 9.6
11 |
12 | TOP_BUTTON_HEIGHT = 13.9
13 | MID_BUTTON_HEIGHT = TOTAL_BUTTON_HEIGHT - TOP_BUTTON_HEIGHT - LOWER_BUTTON_HEIGHT
14 | MID_BUTTON_RAD = 4.8
15 |
16 | CHAMFER_SIZE = 0.5
17 |
18 |
19 | with BuildPart() as button_abxy:
20 | with BuildSketch(Plane.XY.offset(LOWER_BUTTON_HEIGHT)) as bottom_sk:
21 | Circle(LOWER_BUTTON_RAD)
22 | Rectangle(2 * LOWER_BUTTON_RAD, LOWER_BUTTON_WIDTH, mode=Mode.INTERSECT)
23 |
24 | with BuildSketch(Plane.XY.offset(LOWER_BUTTON_HEIGHT + MID_BUTTON_HEIGHT)) as top_sk:
25 | Circle(MID_BUTTON_RAD)
26 |
27 | # Mid section first
28 | loft()
29 |
30 | # Extrude top and bottom
31 | extrude(bottom_sk.sketch, amount=-LOWER_BUTTON_HEIGHT)
32 | extrude(top_sk.sketch, amount=TOP_BUTTON_HEIGHT)
33 |
34 | # Chamfer top
35 | edges = button_abxy.part.edges().group_by(Axis.Z)[-1]
36 | chamfer(edges, CHAMFER_SIZE)
37 |
38 |
39 | if __name__ == '__main__':
40 | from common.vscode import show_object
41 | show_object(button_abxy)
42 |
--------------------------------------------------------------------------------
/build123d/button_dpad.py:
--------------------------------------------------------------------------------
1 | from build123d import (
2 | BuildPart, BuildSketch, BuildLine, Plane, Axis, Polyline,
3 | chamfer, extrude, loft, mirror, make_face, fillet)
4 |
5 |
6 | TOTAL_BUTTON_HEIGHT = 18.3
7 |
8 | LOWER_BUTTON_HEIGHT = 2.4
9 | LOWER_BUTTON_WIDTH = 9.6
10 | LOWER_BUTTON_TIP = 11.54
11 | LOWER_BUTTON_MID = 6.74
12 |
13 | BOTTOM_OUTLINE_PTS = ((0,0), (LOWER_BUTTON_WIDTH / 2, 0),
14 | (LOWER_BUTTON_WIDTH / 2,
15 | LOWER_BUTTON_MID),
16 | (0, LOWER_BUTTON_TIP)) # Gets mirrored
17 |
18 | TOP_BUTTON_HEIGHT = 13.9
19 | TOP_BUTTON_WIDTH = 7.6
20 | TOP_BUTTON_TIP = 10.54
21 | TOP_BUTTON_MID = 6.74
22 |
23 | TOP_OUTLINE_PTS = ((0,0), (TOP_BUTTON_WIDTH / 2, 0),
24 | (TOP_BUTTON_WIDTH / 2,
25 | TOP_BUTTON_MID),
26 | (0, TOP_BUTTON_TIP))
27 |
28 | MID_BUTTON_HEIGHT = TOTAL_BUTTON_HEIGHT - TOP_BUTTON_HEIGHT - LOWER_BUTTON_HEIGHT
29 |
30 | TOP_CHAMFER_SIZE = 0.5
31 | SIDE_FILLET_SIZE = 1
32 |
33 |
34 | with BuildPart() as button_dpad:
35 | with BuildSketch(Plane.XY.offset(LOWER_BUTTON_HEIGHT)) as bottom_sk:
36 | with BuildLine():
37 | Polyline(BOTTOM_OUTLINE_PTS)
38 | mirror(about=Plane.YZ)
39 | make_face()
40 |
41 | with BuildSketch(Plane.XY.offset(LOWER_BUTTON_HEIGHT + MID_BUTTON_HEIGHT)) as top_sk:
42 | with BuildLine() as top_outline:
43 | Polyline(TOP_OUTLINE_PTS)
44 | mirror(about=Plane.YZ)
45 | make_face()
46 | fillet(top_sk.vertices(), SIDE_FILLET_SIZE)
47 |
48 | # Mid section first
49 | loft()
50 |
51 | # Extrude top and bottom
52 | extrude(bottom_sk.sketch, amount=-LOWER_BUTTON_HEIGHT)
53 | extrude(top_sk.sketch, amount=TOP_BUTTON_HEIGHT)
54 |
55 | # Chamfer top
56 | edges = button_dpad.part.edges().group_by(Axis.Z)[-1]
57 | chamfer(edges, TOP_CHAMFER_SIZE)
58 |
59 |
60 | if __name__ == '__main__':
61 | from common.vscode import show_object
62 | show_object(button_dpad)
63 | print(f"Volume: {button_dpad.part.volume}")
64 |
--------------------------------------------------------------------------------
/build123d/button_select.py:
--------------------------------------------------------------------------------
1 | from build123d import (
2 | BuildPart, BuildSketch, Plane, Axis, Rectangle,
3 | chamfer, extrude, loft, fillet)
4 |
5 |
6 | TOTAL_BUTTON_HEIGHT = 16.3
7 |
8 | BUTTON_DEPTH = 7.6
9 |
10 | LOWER_BUTTON_HEIGHT = 2.3
11 | LOWER_BUTTON_WIDTH = 9.6
12 |
13 | TOP_BUTTON_HEIGHT = 12
14 | TOP_BUTTON_WIDTH = 7.6
15 |
16 | MID_BUTTON_HEIGHT = TOTAL_BUTTON_HEIGHT - TOP_BUTTON_HEIGHT - LOWER_BUTTON_HEIGHT
17 |
18 | TOP_CHAMFER_SIZE = 0.5
19 | SIDE_FILLET_SIZE = 1
20 |
21 | with BuildPart() as button_select:
22 | with BuildSketch(Plane.XY.offset(LOWER_BUTTON_HEIGHT)) as bottom_sk:
23 | Rectangle(LOWER_BUTTON_WIDTH, BUTTON_DEPTH)
24 |
25 | with BuildSketch(Plane.XY.offset(LOWER_BUTTON_HEIGHT + MID_BUTTON_HEIGHT)) as top_sk:
26 | Rectangle(TOP_BUTTON_WIDTH, BUTTON_DEPTH)
27 | fillet(top_sk.vertices(), SIDE_FILLET_SIZE)
28 |
29 | # Mid section first
30 | loft()
31 |
32 | # Extrude top and bottom
33 | extrude(bottom_sk.sketch, amount=-LOWER_BUTTON_HEIGHT)
34 | extrude(top_sk.sketch, amount=TOP_BUTTON_HEIGHT)
35 |
36 | # Chamfer top
37 | edges = button_select.part.edges().group_by(Axis.Z)[-1]
38 | chamfer(edges, TOP_CHAMFER_SIZE)
39 |
40 |
41 | if __name__ == '__main__':
42 | from common.vscode import show_object
43 | show_object(button_select)
44 | print(f"Volume: {button_select.part.volume}")
45 |
--------------------------------------------------------------------------------
/build123d/common/vscode.py:
--------------------------------------------------------------------------------
1 | from ocp_vscode import set_defaults, Camera
2 |
3 | # Functions available to the importer.
4 | from ocp_vscode import show_object, show_all
5 |
6 | # Do not reset camera.
7 | set_defaults(reset_camera=Camera.KEEP)
8 |
--------------------------------------------------------------------------------
/build123d/cover.py:
--------------------------------------------------------------------------------
1 | from build123d import (
2 | BuildPart, BuildSketch, BuildLine, Box, Plane, Polyline, Rectangle, Location, Locations,
3 | Axis, Rot, Mode, Align, Until,
4 | mirror, make_face, extrude, fillet, chamfer, split, faces, add)
5 |
6 | COVER_BOTTOM_WIDTH = 62
7 | COVER_BOTTOM_DEPTH = 37
8 | COVER_BOTTOM_HEIGHT = 1.6
9 |
10 | TRAPEZ_WIDTH = 25
11 | TRAPEZ_PTS = ((0, 0), (TRAPEZ_WIDTH, 0), (20, 5), (5, 5), (0, 0))
12 |
13 | BOX_WIDTH = 6
14 | BOX_DEPTH = 6
15 | BOX_HEIGHT = 8
16 | BOX_INSET = 8 # From outside
17 |
18 | USE_TABS = True
19 | TAB_WIDTH = 4
20 | TAB_HEIGHT = 2
21 | TAB_ANGLE = 5 # Degrees.
22 |
23 | FILLET_SIZE = 2
24 | CHAMFER_SIZE = 0.5
25 |
26 |
27 | with BuildPart() as cover:
28 | # Base.
29 | with BuildSketch():
30 | Rectangle(COVER_BOTTOM_WIDTH, COVER_BOTTOM_DEPTH)
31 | extrude(amount=COVER_BOTTOM_HEIGHT)
32 | split(bisect_by=Plane.YZ) # keep only half, since everything will be mirrored in the end
33 |
34 | # Trapezoid.
35 | plane = Plane.XZ.offset(-COVER_BOTTOM_DEPTH / 2)
36 | with BuildSketch(plane) as poly_sk:
37 | with BuildLine(Location((COVER_BOTTOM_WIDTH / 2 - TRAPEZ_WIDTH, COVER_BOTTOM_HEIGHT))):
38 | Polyline(TRAPEZ_PTS)
39 | make_face()
40 | extrude(amount=COVER_BOTTOM_HEIGHT)
41 |
42 | # Trapezoid tabs.
43 | if USE_TABS:
44 | trapezoid_face = poly_sk.sketch.face().center()
45 | # Manually construct a plane with some of the face center coordinates.
46 | plane = Plane(origin=(
47 | trapezoid_face.X,
48 | trapezoid_face.Y,
49 | COVER_BOTTOM_HEIGHT + 2),
50 | x_dir=(-1, 0, 0),
51 | z_dir=(0, 1, 0),
52 | )
53 | with BuildSketch(plane.rotated((TAB_ANGLE, 0, 0))): # create sketch on plane rotated about local y-axis
54 | Rectangle(TAB_WIDTH, TAB_HEIGHT, align=(Align.CENTER, Align.MAX)) # align top edge to y-axis
55 | extrude(until=Until.PREVIOUS)
56 |
57 | # Box.
58 | with BuildPart() as box_pt:
59 | with BuildSketch() as s:
60 | x = COVER_BOTTOM_WIDTH / 2 - BOX_INSET - BOX_WIDTH / 2
61 | y = -COVER_BOTTOM_DEPTH / 2 - BOX_WIDTH / 2
62 | with Locations((x, y)):
63 | Rectangle(BOX_WIDTH, BOX_DEPTH)
64 | extrude(amount=BOX_HEIGHT)
65 |
66 | # Box tabs.
67 | if USE_TABS:
68 | face_inside = faces().filter_by(Axis.X)[0]
69 | face_outside = faces().filter_by(Axis.X)[-1]
70 | plane_inside = Plane(face_inside).rotated((180, -TAB_ANGLE, 0))
71 | plane_outside = Plane(face_outside).rotated((0, -TAB_ANGLE, 0))
72 | with BuildSketch(plane_inside):
73 | Rectangle(TAB_WIDTH, TAB_HEIGHT, align=(Align.CENTER, Align.MAX))
74 | with BuildSketch(plane_outside):
75 | Rectangle(TAB_WIDTH, TAB_HEIGHT, align=(Align.CENTER, Align.MAX))
76 | extrude(until=Until.PREVIOUS)
77 |
78 | # Fillet trapezoid sides.
79 | edges = cover.part.edges().filter_by(Axis.X).group_by(Axis.Y)[2][0]
80 | fillet(edges, FILLET_SIZE)
81 |
82 | # Fillet boxes side.
83 | edges = cover.part.edges().filter_by(Axis.X).group_by(Axis.Y)[1][1]
84 | fillet(edges, FILLET_SIZE)
85 |
86 | # Chamfer box top.
87 | edges = cover.part.edges().filter_by(Axis.Z, reverse=True).group_by(Axis.Z)[-1]
88 | chamfer(edges, CHAMFER_SIZE)
89 |
90 | # Mirror everything.
91 | mirror(about=Plane.YZ)
92 |
93 |
94 | if __name__ == '__main__':
95 | from common.vscode import show_object
96 | show_object(cover)
97 | print(f"Volume: {cover.part.volume}")
98 |
--------------------------------------------------------------------------------
/build123d/dongle_case.py:
--------------------------------------------------------------------------------
1 | from build123d import (
2 | BuildPart, BuildSketch, BuildLine, Polyline, Plane, Rectangle, Locations,
3 | Mode, Circle, Align, extrude, mirror, make_face
4 | )
5 |
6 | THICKNESS_AROUND = 1.2
7 | THICKNESS_END = 0.5
8 |
9 | PCB_WIDTH = 21.8
10 | PCB_DEPTH = 46.9
11 | PCB_THICKNESS = 1.65
12 |
13 | DONGLE_HEIGHT = 4.12 # @ button
14 | INSET = DONGLE_HEIGHT - PCB_THICKNESS
15 |
16 | TOLERANCE_WIDTH = +0.35
17 | TOLERANCE_HEIGHT = +0.1
18 |
19 | ANTENNA_WIDTH = 13.2 + 0.75 # measured + tolerance
20 | ANTENNA_DEPTH = 5.2 + 0.1
21 |
22 | EJECTION_HOLE_SIZE = 2.3 # slightly larger than hex-key
23 |
24 | CUT_WIDTH = 0.3
25 | CUT_LONG = 31.2
26 | CUT_SHORT = 15.6
27 |
28 | BUMP_WIDTH = 4
29 | BUMP_OFFSET = 1
30 |
31 | MAIN_PTS_OUTER = (
32 | (0, 0),
33 | (PCB_WIDTH/2 + TOLERANCE_WIDTH + THICKNESS_AROUND, 0),
34 | (PCB_WIDTH/2 + TOLERANCE_WIDTH + THICKNESS_AROUND, THICKNESS_AROUND + PCB_THICKNESS + 2 * TOLERANCE_HEIGHT),
35 | (PCB_WIDTH/2 + TOLERANCE_WIDTH - INSET, DONGLE_HEIGHT + 2 * TOLERANCE_HEIGHT + 2 * THICKNESS_AROUND),
36 | (0, DONGLE_HEIGHT + 2 * TOLERANCE_HEIGHT + 2 * THICKNESS_AROUND)
37 | )
38 |
39 | # Not symmetric due to button => Need to specify all points
40 | MAIN_PTS_INNER = (
41 | (-PCB_WIDTH/2 - TOLERANCE_WIDTH, THICKNESS_AROUND),
42 | ( PCB_WIDTH/2 + TOLERANCE_WIDTH, THICKNESS_AROUND),
43 | ( PCB_WIDTH/2 + TOLERANCE_WIDTH, PCB_THICKNESS + THICKNESS_AROUND + 2 * TOLERANCE_HEIGHT),
44 | ( PCB_WIDTH/2 - INSET/2 + TOLERANCE_WIDTH, DONGLE_HEIGHT + THICKNESS_AROUND - INSET/2 + 2 * TOLERANCE_HEIGHT),
45 | ( PCB_WIDTH/2 - INSET*1.5, DONGLE_HEIGHT + THICKNESS_AROUND + 2 * TOLERANCE_HEIGHT),
46 | (-PCB_WIDTH/2 - TOLERANCE_WIDTH + INSET, DONGLE_HEIGHT + THICKNESS_AROUND + 2 * TOLERANCE_HEIGHT),
47 | (-PCB_WIDTH/2 - TOLERANCE_WIDTH, PCB_THICKNESS + THICKNESS_AROUND + 2 * TOLERANCE_HEIGHT),
48 | (-PCB_WIDTH/2 - TOLERANCE_WIDTH, THICKNESS_AROUND - TOLERANCE_HEIGHT)
49 | )
50 |
51 |
52 | with BuildPart() as dongle_case:
53 | # Main body
54 | with BuildSketch(Plane.XZ):
55 | with BuildLine():
56 | Polyline(MAIN_PTS_OUTER)
57 | mirror(about=Plane.YZ)
58 | make_face()
59 | total_depth = PCB_DEPTH + ANTENNA_DEPTH + THICKNESS_END
60 | extrude(amount=total_depth)
61 |
62 | # Remove interior
63 | with BuildSketch(Plane.XZ):
64 | with BuildLine():
65 | Polyline(MAIN_PTS_INNER)
66 | make_face()
67 | extrude(amount=PCB_DEPTH, mode=Mode.SUBTRACT)
68 |
69 | # Remove space for Antenna
70 | with BuildSketch(Plane.XZ.offset(PCB_DEPTH)):
71 | z = THICKNESS_AROUND + DONGLE_HEIGHT/2 + TOLERANCE_HEIGHT
72 | with Locations((0, z)):
73 | Rectangle(ANTENNA_WIDTH, DONGLE_HEIGHT + 2 * TOLERANCE_HEIGHT)
74 | extrude_amount = ANTENNA_DEPTH
75 | extrude(amount=extrude_amount, mode=Mode.SUBTRACT)
76 |
77 | # Ejection holes
78 | with BuildSketch(Plane.XZ.offset(PCB_DEPTH)):
79 | z = THICKNESS_AROUND + EJECTION_HOLE_SIZE / 2 + 0.1
80 | location1 = (-ANTENNA_WIDTH / 2 - 1.5, z)
81 | location2 = ( ANTENNA_WIDTH / 2 + 1.5, z)
82 | with Locations(location1, location2):
83 | Rectangle(EJECTION_HOLE_SIZE, EJECTION_HOLE_SIZE + 0.2)
84 | extrude_amount = -ANTENNA_DEPTH - THICKNESS_END
85 | extrude(amount=extrude_amount, both=True, mode=Mode.SUBTRACT)
86 |
87 | # Cuts to make bottom flexible => can press PCB + button against top
88 | x = PCB_WIDTH/2 + TOLERANCE_WIDTH
89 | with BuildSketch(Plane.XZ): # Short cut.
90 | with Locations((x, 0)):
91 | Rectangle(CUT_WIDTH, THICKNESS_AROUND, align=(Align.MAX, Align.MIN))
92 | extrude(amount=CUT_SHORT, mode=Mode.SUBTRACT)
93 | with BuildSketch(Plane.XZ): # Long cut.
94 | with Locations((-x, 0)):
95 | Rectangle(CUT_WIDTH, THICKNESS_AROUND, align=(Align.MIN, Align.MIN))
96 | extrude(amount=CUT_LONG, mode=Mode.SUBTRACT)
97 |
98 | # Bump to hold PCB in place.
99 | with BuildSketch(Plane.YZ):
100 | y = -PCB_THICKNESS/2 - BUMP_WIDTH/2 + BUMP_OFFSET
101 | z = THICKNESS_AROUND
102 | with Locations((y, z)):
103 | Circle(PCB_THICKNESS/2)
104 | extrude_amount = PCB_WIDTH/2 + TOLERANCE_WIDTH - CUT_WIDTH
105 | extrude(amount=extrude_amount, both=True)
106 |
107 | # Bump cut with the PCB shape.
108 | with BuildSketch(Plane.XY.offset(THICKNESS_AROUND)) as xxx1:
109 | with BuildLine():
110 | x = PCB_WIDTH/2 + TOLERANCE_WIDTH - BUMP_WIDTH
111 | y = -BUMP_WIDTH/2 - PCB_THICKNESS + BUMP_OFFSET
112 | Polyline(
113 | ( x + PCB_THICKNESS, y),
114 | (-x - PCB_THICKNESS, y),
115 | (-x, y + PCB_THICKNESS),
116 | ( x, y + PCB_THICKNESS),
117 | ( x + PCB_THICKNESS, y)
118 | )
119 | make_face()
120 | extrude(amount=PCB_THICKNESS/2, mode=Mode.SUBTRACT)
121 |
122 |
123 | if __name__ == '__main__':
124 | from common.vscode import show_object
125 | show_object(dongle_case)
126 | print(f"Volume: {dongle_case.part.volume}")
127 |
--------------------------------------------------------------------------------
/build123d/hexagon.py:
--------------------------------------------------------------------------------
1 | from build123d import *
2 | import math
3 |
4 | BASE_X_LEN = 36
5 | BASE_Y_LEN = 40
6 | BASE_Z_LEN = 1.9
7 | BASE_Y_SIDE = 20
8 |
9 | HOLE_RADIUS = 5
10 | HOLE_DISTANCE_FROM_CENTER = 12
11 |
12 | TRAPEZ_THICKNESS = 2.758 ## Tight version was 2.8.
13 | TRAPEZ_X_LEN = math.sqrt(10**2 + 10**2) # Diagonal of 10x10 (Blender legacy).
14 | TRAPEZ_Z_LEN = 18
15 | TRAPEZ_TIP = 2.2
16 |
17 | TABS_Z_LEN = 4
18 |
19 |
20 | with BuildPart() as chex:
21 | # Base.
22 | with BuildSketch():
23 | with BuildLine():
24 | Polyline([
25 | (0, BASE_Y_LEN/2),
26 | (BASE_X_LEN/2, BASE_Y_SIDE/2),
27 | (BASE_X_LEN/2, 0),
28 | ])
29 | mirror(about=Plane.YZ)
30 | mirror(about=Plane.XZ)
31 | make_face()
32 | # Holes.
33 | locations = [
34 | (0, HOLE_DISTANCE_FROM_CENTER),
35 | (0, -HOLE_DISTANCE_FROM_CENTER),
36 | (HOLE_DISTANCE_FROM_CENTER, 0),
37 | (-HOLE_DISTANCE_FROM_CENTER, 0),
38 | ]
39 | with Locations(locations):
40 | Circle(HOLE_RADIUS, mode=Mode.SUBTRACT)
41 | extrude(amount=BASE_Z_LEN)
42 |
43 | # Trapezoid.
44 | with BuildPart(mode=Mode.PRIVATE) as trapezoid_pt:
45 | with BuildSketch(Plane.XZ):
46 | with BuildLine():
47 | points = (
48 | (0, 0),
49 | (TRAPEZ_X_LEN, 0),
50 | (TRAPEZ_TIP, TRAPEZ_Z_LEN - BASE_Z_LEN),
51 | (0, TRAPEZ_Z_LEN - BASE_Z_LEN),
52 | )
53 | Polyline(points)
54 | mirror(about=Plane.YZ)
55 | make_face()
56 | extrude(amount=TRAPEZ_THICKNESS/2, both=True)
57 | with Locations((0, 0, BASE_Z_LEN)):
58 | add(trapezoid_pt, rotation=(0, 0, -45))
59 | add(trapezoid_pt, rotation=(0, 0, 45))
60 |
61 | # Tabs (to prevent wrong insertion).
62 | tab_z = TRAPEZ_Z_LEN - TABS_Z_LEN
63 | normal = Axis(origin=(0,0,0), direction=(1,1,0))
64 | face1 = (chex.faces()
65 | .filter_by(normal)[1]
66 | .split(Plane.XY.offset(tab_z), keep=Keep.TOP)
67 | )
68 | face2 = (chex.faces()
69 | .filter_by(normal)[3]
70 | .split(Plane.XY.offset(tab_z), keep=Keep.TOP)
71 | )
72 | extrude(face1, until=Until.NEXT, dir=(0,1,0))
73 | extrude(face2, until=Until.NEXT, dir=(0,-1,0))
74 |
75 |
76 | if __name__ == '__main__':
77 | from common.vscode import show_object
78 | show_object(chex)
79 | print(f"Volume: {chex.part.volume}")
80 | # export_stl(chex.part, 'stl/test_hex.stl')
81 |
--------------------------------------------------------------------------------
/build123d/thumbstick_right.py:
--------------------------------------------------------------------------------
1 | from build123d import *
2 |
3 | TOTAL_HEIGHT = 8.75
4 |
5 | HEAD_RADIUS = 4
6 | HEAD_TALL = 2
7 | HEAD_CHAMFER = 0.4
8 | NECK_RADIUS = 2.5
9 |
10 | DOME_RADIUS = 12
11 | DOME_TOP_HEIGHT = 1.85
12 |
13 | HOLE_TOLERANCE_XY = 0.1
14 | HOLE_TOLERANCE_Z = 0.5
15 | HOLE_RADIUS = 2 + HOLE_TOLERANCE_XY
16 | HOLE_CUT = 1.5 + HOLE_TOLERANCE_XY
17 | HOLE_DEPTH = 5 + HOLE_TOLERANCE_Z
18 |
19 |
20 | with BuildPart() as thumbstick_right:
21 | # Dome.
22 | with BuildSketch(Plane.XZ) as dome:
23 | with Locations((0, -DOME_RADIUS + DOME_TOP_HEIGHT)):
24 | Circle(radius=DOME_RADIUS)
25 | split(bisect_by=Plane.XZ, keep=Keep.BOTTOM)
26 | split(bisect_by=Plane.YZ)
27 |
28 | # Neck and head.
29 | with BuildSketch(Plane.XZ) as shaft:
30 | # Neck.
31 | Rectangle(NECK_RADIUS, TOTAL_HEIGHT, align=Align.MIN)
32 | # Head.
33 | with BuildLine():
34 | Polyline(
35 | (0, TOTAL_HEIGHT),
36 | (HEAD_RADIUS, TOTAL_HEIGHT),
37 | (HEAD_RADIUS, TOTAL_HEIGHT - HEAD_TALL),
38 | (0, TOTAL_HEIGHT - HEAD_TALL - HEAD_RADIUS),
39 | )
40 | make_face()
41 |
42 | # Make 3D.
43 | revolve(axis=Axis.Z)
44 |
45 | # Remove hole.
46 | with BuildSketch(Plane.XY) as hole:
47 | Circle(radius=HOLE_RADIUS)
48 | Rectangle(HOLE_RADIUS * 2, HOLE_CUT * 2, mode=Mode.INTERSECT)
49 | extrude(amount=HOLE_DEPTH, mode=Mode.SUBTRACT)
50 |
51 | # Chamfer top edge.
52 | top_edge = edges().sort_by(Axis.Z)[-1]
53 | chamfer(top_edge, HEAD_CHAMFER)
54 |
55 |
56 | if __name__ == '__main__':
57 | from common.vscode import show_object
58 | show_object(thumbstick_right, name='Thumbstick')
59 | # export_stl(thumbstick_right.part, 'stl/test_thumbstick_right.stl')
60 |
--------------------------------------------------------------------------------
/build123d/trigger_r1.py:
--------------------------------------------------------------------------------
1 | from build123d import (
2 | add, Axis, BuildLine, BuildPart, BuildSketch, Circle, chamfer, edges,
3 | faces, extrude, make_face, Mode, Plane, Polyline
4 | )
5 |
6 | SHAFT_RADIUS = 3
7 | SHAFT_TOLERANCE = 0.1
8 | SHAFT_Z = 13
9 | SHAFT_TOP_CHAMFER = 0.5
10 |
11 | BRIDGE_Z = 9.4
12 | BRIDGE_RELIEF_Z = 0.9 # Cut-out to avoid potential 3D-printed overhangs.
13 | BRIDGE_CONTACT_TOLERANCE_Y = 0.3 # Contact with the button switch.
14 |
15 | SLAB_X_0 = 11
16 | SLAB_X_1 = 17
17 | SLAB_X_2 = 24
18 | SLAB_X_3 = 29.5
19 | SLAB_Y_0 = 4
20 | SLAB_Y_1 = 9
21 | SLAB_Z = 13
22 | SLAB_Z_MISALIGN = 0.3 # Error in the original design?
23 | SLAB_CHAMFER = 0.5
24 |
25 | TOLERANCE_X = 0.1 # Applied only to some parts of the bridge and slab.
26 | TOLERANCE_Y = 0.1 # Applied only to some parts of the bridge and slab.
27 | TOLERANCE_Z = 0.7 # Applied to the height of all parts.
28 |
29 | BRIDGE_POINTS = [
30 | (0, 0),
31 | (0, 1 - TOLERANCE_Y),
32 | (SLAB_X_0 + TOLERANCE_X, SLAB_Y_0 - TOLERANCE_Y),
33 | (SLAB_X_1, SLAB_Y_0 - TOLERANCE_Y),
34 | (SLAB_X_1, 2),
35 | (13.5, -2 + BRIDGE_CONTACT_TOLERANCE_Y),
36 | (8, -2 + BRIDGE_CONTACT_TOLERANCE_Y),
37 | (6, -3 + TOLERANCE_Y),
38 | (0, -3 + TOLERANCE_Y),
39 | (0, 0),
40 | ]
41 |
42 | RELIEF_POINTS = [
43 | (6, -3),
44 | (6, -2),
45 | (8, -1),
46 | (SLAB_X_1, 0),
47 | (SLAB_X_1, -3),
48 | (6, -3)
49 | ]
50 |
51 | SLAB_XY_POINTS = [
52 | (SLAB_X_0 + TOLERANCE_X, SLAB_Y_0 - TOLERANCE_Y),
53 | (SLAB_X_0 + TOLERANCE_X, SLAB_Y_1),
54 | (SLAB_X_3 - TOLERANCE_X, SLAB_Y_1),
55 | (SLAB_X_3 - TOLERANCE_X, 5),
56 | (SLAB_X_2, 3.5),
57 | (SLAB_X_1, 3.5),
58 | (SLAB_X_0 + TOLERANCE_X, SLAB_Y_0 - TOLERANCE_Y),
59 | ]
60 |
61 | SLAB_XZ_POINTS = [
62 | (SLAB_XY_POINTS[0][0], 0),
63 | (SLAB_XY_POINTS[1][0], BRIDGE_Z - TOLERANCE_Z),
64 | (SLAB_XY_POINTS[5][0], SLAB_Z - TOLERANCE_Z),
65 | (SLAB_XY_POINTS[4][0], SLAB_Z - TOLERANCE_Z),
66 | (SLAB_XY_POINTS[2][0], BRIDGE_Z - TOLERANCE_Z + SLAB_Z_MISALIGN),
67 | (SLAB_XY_POINTS[3][0], 0),
68 | (SLAB_XY_POINTS[0][0], 0),
69 | ]
70 |
71 | with BuildPart() as internal:
72 | # Shaft.
73 | with BuildSketch():
74 | Circle(SHAFT_RADIUS - SHAFT_TOLERANCE)
75 | extrude(amount=SHAFT_Z - TOLERANCE_Z)
76 | shaft_top = faces().filter_by(Axis.Z).sort_by(Axis.Z).edges()[-1]
77 | chamfer(shaft_top, SHAFT_TOP_CHAMFER)
78 | # Bridge.
79 | with BuildSketch():
80 | with BuildLine():
81 | Polyline(BRIDGE_POINTS)
82 | make_face()
83 | extrude(amount=BRIDGE_Z - TOLERANCE_Z)
84 | # Relief.
85 | plane = Plane.XY.offset(BRIDGE_Z - TOLERANCE_Z - BRIDGE_RELIEF_Z)
86 | with BuildSketch(plane):
87 | with BuildLine():
88 | Polyline(RELIEF_POINTS)
89 | make_face()
90 | extrude(amount=BRIDGE_RELIEF_Z, mode=Mode.SUBTRACT)
91 |
92 | with BuildPart() as slab:
93 | # Slab XY.
94 | with BuildSketch():
95 | with BuildLine():
96 | Polyline(SLAB_XY_POINTS)
97 | make_face()
98 | extrude(amount=SLAB_Z - TOLERANCE_Z)
99 | # Slab XZ (intersects with XY).
100 | with BuildSketch(Plane.XZ):
101 | with BuildLine():
102 | Polyline(SLAB_XZ_POINTS)
103 | make_face()
104 | until = max([y for (x,y) in SLAB_XY_POINTS]) # Further away point in XY.sketch.
105 | extrude(amount=-until, mode=Mode.INTERSECT)
106 | # Chamfer.
107 | edges1 = faces().filter_by(Plane.XZ)[1].edges() # Touchy face.
108 | edges2 = faces().filter_by(Plane.XY)[0].edges().filter_by(Axis.Y) # Bottom edges.
109 | chamfer(edges1 + edges2, SLAB_CHAMFER)
110 |
111 | # Combine both parts.
112 | with BuildPart() as trigger_r1:
113 | add(internal.part)
114 | add(slab.part)
115 |
116 |
117 | if __name__ == '__main__':
118 | from common.vscode import show_object
119 | show_object(trigger_r1)
120 |
--------------------------------------------------------------------------------
/build123d/wheel.py:
--------------------------------------------------------------------------------
1 | from build123d import *
2 | from math import cos, pi
3 |
4 | WHEEL_INDENTS = 24
5 | WHEEL_RADIUS_OUTER = 10.75
6 | WHEEL_RADIUS_INNER = 10.25
7 | WHEEL_WIDTH = 6.75
8 | WHEEL_CHAMFER = 0.75
9 |
10 | HEX_DIAMETER = 2 # Previously 2.05
11 | HEX_DIAMETER_SHORT = HEX_DIAMETER * cos(pi / 6)
12 |
13 | LEFT_PADDING_RADIUS = 3.5
14 | LEFT_PADDING_WIDTH = 2.5
15 |
16 | RIGHT_HOLE_RADIUS = 1.5
17 | RIGHT_HOLE_RADIUS_TOLERANCE = 0.15
18 | RIGHT_HOLE_DEPTH = 4.5
19 |
20 | LEFT_SLOT_WIDTH = 9
21 | LEFT_SLOT_UNDER_WIDTH = 5.5
22 | LEFT_SLOT_DEPTH = WHEEL_WIDTH - RIGHT_HOLE_DEPTH
23 |
24 | # Left slot tolerance (with recommendations).
25 | LEFT_SLOT_TOLERANCE_DEFAULT = 0.00
26 | LEFT_SLOT_TOLERANCE_LOOSE = 0.05
27 | LEFT_SLOT_TOLERANCE_TIGHT = -0.05
28 |
29 |
30 | def build_wheel(LEFT_SLOT_TOLERANCE):
31 | with BuildPart() as wheel:
32 | with BuildSketch():
33 | # Create a single indent chevron.
34 | with BuildLine(mode=Mode.PRIVATE) as line:
35 | # Create indent line.
36 | a = (WHEEL_RADIUS_OUTER, 0)
37 | b = PolarLine(
38 | start=(0, 0),
39 | length=WHEEL_RADIUS_INNER,
40 | angle=(180 / WHEEL_INDENTS),
41 | mode=Mode.PRIVATE,
42 | ) @ 1 # Point from line workaround.
43 | Line(a, b)
44 | # Mirror line into a chevron.
45 | mirror(about=Plane.YZ)
46 | # Repeat chevrons around a circle.
47 | with PolarLocations(0, WHEEL_INDENTS):
48 | add(line.line)
49 | make_face()
50 | extrude(amount=WHEEL_WIDTH)
51 |
52 | # Wheel chamfer.
53 | side_edges = edges().filter_by(Axis.Z, reverse=True)
54 | chamfer(side_edges, WHEEL_CHAMFER)
55 |
56 | # Right hole.
57 | with BuildSketch():
58 | Circle(RIGHT_HOLE_RADIUS + RIGHT_HOLE_RADIUS_TOLERANCE)
59 | extrude(amount=RIGHT_HOLE_DEPTH, mode=Mode.SUBTRACT)
60 |
61 | # Left aperture (in which the core is inserted).
62 | with BuildSketch(Plane.XY.offset(WHEEL_WIDTH)):
63 | with Locations(Rotation(0, 0, 45)):
64 | side = LEFT_SLOT_WIDTH + (LEFT_SLOT_TOLERANCE*2)
65 | Rectangle(side, side)
66 | cut = Plane.XZ.offset((HEX_DIAMETER_SHORT/2) + LEFT_SLOT_TOLERANCE)
67 | split(bisect_by=cut, keep=Keep.BOTTOM)
68 | with Locations(Rotation(0, 0, 45)):
69 | Rectangle(LEFT_SLOT_UNDER_WIDTH, LEFT_SLOT_UNDER_WIDTH)
70 | extrude(amount=-LEFT_SLOT_DEPTH, mode=Mode.SUBTRACT)
71 | return wheel
72 |
73 | wheel_default = build_wheel(LEFT_SLOT_TOLERANCE_DEFAULT)
74 | wheel_loose = build_wheel(LEFT_SLOT_TOLERANCE_LOOSE)
75 | wheel_tight = build_wheel(LEFT_SLOT_TOLERANCE_TIGHT)
76 |
77 |
78 | if __name__ == '__main__':
79 | from common.vscode import show_object
80 | from wheel_holder import holder
81 | from wheel_core import core
82 | show_object(wheel_default, name='Wheel')
83 | show_object(holder, name='Holder')
84 | show_object(core, name='Core')
85 | # export_stl(wheel.part, 'stl/test_wheel.stl')
86 | # export_stl(support.part, 'stl/test_support.stl')
87 | # export_stl(core.part, 'stl/test_core.stl')
88 |
--------------------------------------------------------------------------------
/build123d/wheel_core.py:
--------------------------------------------------------------------------------
1 | from build123d import *
2 | from math import cos, pi
3 |
4 | from wheel import (
5 | HEX_DIAMETER,
6 | HEX_DIAMETER_SHORT,
7 | WHEEL_WIDTH,
8 | LEFT_SLOT_WIDTH,
9 | LEFT_SLOT_DEPTH,
10 | )
11 |
12 | HEX_LEN = 8
13 | SPACER_RADIUS = 2
14 | SPACER_DEPTH = 1
15 |
16 |
17 | with BuildPart() as core:
18 | # Hex axle.
19 | with BuildSketch(Plane.XY.offset(WHEEL_WIDTH)):
20 | RegularPolygon(HEX_DIAMETER / 2, 6)
21 | extrude(amount=HEX_LEN)
22 |
23 | # Body.
24 | with BuildSketch(Plane.XY.offset(WHEEL_WIDTH)):
25 | Circle(LEFT_SLOT_WIDTH / 2)
26 | cutplane = Plane.XZ.offset(HEX_DIAMETER_SHORT / 2)
27 | split(bisect_by=cutplane, keep=Keep.BOTTOM)
28 | extrude(amount=-LEFT_SLOT_DEPTH)
29 |
30 | # Spacer.
31 | with BuildSketch(Plane.XY.offset(WHEEL_WIDTH)):
32 | Circle(SPACER_RADIUS)
33 | cutplane = Plane.XZ.offset(HEX_DIAMETER_SHORT / 2)
34 | split(bisect_by=cutplane, keep=Keep.BOTTOM)
35 | extrude(amount=SPACER_DEPTH)
36 |
37 | if __name__ == '__main__':
38 | from common.vscode import show_object
39 | from wheel import wheel
40 | show_object(core, name="Core")
41 | show_object(wheel, name="Wheel")
42 |
--------------------------------------------------------------------------------
/build123d/wheel_holder.py:
--------------------------------------------------------------------------------
1 | from build123d import *
2 |
3 | from wheel import RIGHT_HOLE_RADIUS as AXLE_RADIUS
4 | AXLE_LEN = 1.5
5 | AXLE_X_OFFSET = 0.5
6 | AXLE_Y_OFFSET = 11
7 | AXLE_CHAMFER = 0.3
8 |
9 | BODY_X_LEN = 18
10 | BODY_Z_LEN = 3.1 # Adjust axis left/right (positive = to the left).
11 | BODY_Y_TOLERANCE = -0.10 # Adjust axis height (negative = higher).
12 |
13 | SEPARATOR_X_LEN = 12.5
14 | SEPARATOR_Z_LEN = 0.5
15 | SEPARATOR_CORNER_RADIUS = 1
16 |
17 | BLOCK_X = 9.6
18 | BLOCK_Y = 15
19 | BLOCK_Y_OFFSET = 0.3
20 |
21 | CHAMFER_Y = 0.8
22 | CHAMFER_Z = 2.9
23 |
24 | # Translated plane by X offset.
25 | workplane = Plane(origin=(AXLE_X_OFFSET, 0))
26 |
27 | with BuildPart() as holder:
28 | # Body.
29 | with BuildSketch(workplane.offset(-SEPARATOR_Z_LEN)) as body_s:
30 | with Locations((0, -AXLE_Y_OFFSET + BODY_Y_TOLERANCE)):
31 | Rectangle(
32 | BODY_X_LEN,
33 | AXLE_Y_OFFSET + AXLE_RADIUS - BODY_Y_TOLERANCE,
34 | align=(Align.CENTER, Align.MIN),
35 | )
36 | extrude(amount=-BODY_Z_LEN)
37 |
38 | # Chamfer from bed.
39 | edge_bed = (edges()
40 | .filter_by(Axis.X)
41 | .group_by(Axis.Z)[0]
42 | .sort_by(Axis.Y, reverse=True)[0]
43 | )
44 | chamfer(edge_bed, length=CHAMFER_Z, length2=CHAMFER_Y)
45 |
46 | # Fillet body corners
47 | edges_corner = (edges()
48 | .filter_by(Axis.Z)
49 | .group_by(Axis.Y, reverse=True)[0]
50 | )
51 | fillet(edges_corner, radius=4)
52 |
53 | # Remove thumbstick block.
54 | with BuildSketch(workplane.offset(-SEPARATOR_Z_LEN - BODY_Z_LEN)):
55 | with Locations((0, BLOCK_Y_OFFSET)):
56 | Rectangle(BLOCK_X, BLOCK_Y, align=(Align.CENTER, Align.MAX))
57 | extrude(amount=BLOCK_Y, mode=Mode.SUBTRACT)
58 |
59 | # Separator.
60 | with BuildSketch(workplane):
61 | with Locations((0, AXLE_RADIUS)):
62 | RectangleRounded(
63 | SEPARATOR_X_LEN,
64 | AXLE_RADIUS * 2,
65 | radius=SEPARATOR_CORNER_RADIUS,
66 | align=(Align.CENTER, Align.MAX)
67 | )
68 | extrude(amount=-SEPARATOR_Z_LEN)
69 |
70 | # Axle.
71 | with BuildSketch():
72 | Circle(AXLE_RADIUS)
73 | extrude(amount=AXLE_LEN)
74 |
75 | # Axle chamfer.
76 | edge = edges().group_by(Axis.Z, reverse=True)[0]
77 | chamfer(edge, AXLE_CHAMFER)
78 |
79 |
80 | if __name__ == '__main__':
81 | from common.vscode import show_object
82 | from wheel import wheel
83 | show_object(holder, name="Holder")
84 | show_object(wheel, name="Wheel")
85 |
--------------------------------------------------------------------------------
/contributor.md:
--------------------------------------------------------------------------------
1 | # Contributor Agreement
2 |
3 | ## Human-readable summary
4 |
5 | The TLDR of the agreement is:
6 |
7 | - You confirm that you are the author of the contributions you are submitting.
8 | - You retain the copyright of your contributions, but you give us permission to use such contributions without restrictions.
9 | - You won't use any additional terms or patents against us.
10 | - We will always keep your contributions under the current license, or some other open source license.
11 |
12 | This summary (content before Section 0) is not part of the agreement, and it is NOT legally binding. It may be incomplete or even incorrect. It does not in any case override the actual legal terms defined below. Keep reading for the actual legal terms.
13 |
14 | ## 0. Preface
15 | Thank you for your interest in contributing to Input Labs ("We" or "Us").
16 |
17 | This contributor agreement ("Agreement") documents the rights granted by contributors to Us. To make this document effective, please follow the instructions at Section 7.2. This is a legally binding document (Sections 0 and subsequent), so please read it carefully before agreeing to it. The Agreement may cover more than one software project managed by Us.
18 |
19 | ## 1. Definitions
20 | "You" (as Individual) means the individual who Submits a Contribution to Us.
21 |
22 | "You" (as Entity) means any Legal Entity on behalf of whom a Contribution has been received by Us. "Legal Entity" means an entity which is not a natural person. "Affiliates" means other Legal Entities that control, are controlled by, or under common control with that Legal Entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such Legal Entity, whether by contract or otherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities which vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial ownership of such entity.
23 |
24 | "Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, please follow the instructions in Section 7.1.
25 |
26 | "Copyright" means all rights protecting works of authorship owned or controlled by You [or Your Affiliates], including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You.
27 |
28 | "Material" means the work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, the Material means the work of authorship to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material.
29 |
30 | "Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
31 |
32 | "Submission Date" means the date on which You Submit a Contribution to Us.
33 |
34 | "Effective Date" means the date You execute this Agreement or the date You first Submit a Contribution to Us, whichever is earlier.
35 |
36 | "Media" means any portion of a Contribution which is not software.
37 |
38 | ## 2. Grant of Rights
39 |
40 | ### 2.1 Copyright License
41 | (a) You retain ownership of the Copyright in Your Contribution and have the same rights to use or license the Contribution which You would have had without entering into the Agreement.
42 |
43 | (b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform and distribute the Contribution as part of the Material; provided that this license is conditioned upon compliance with Section 2.3.
44 |
45 | ### 2.1 Copyright Assignment
46 | (a) At the time the Contribution is Submitted, You assign to Us all right, title, and interest worldwide in all Copyright covering the Contribution; provided that this transfer is conditioned upon compliance with Section 2.3.
47 |
48 | (b) To the extent that any of the rights in Section 2.1(a) cannot be assigned by You to Us, You grant to Us a perpetual, worldwide, exclusive, royalty-free, transferable, irrevocable license under such non-assigned rights, with rights to sublicense through multiple tiers of sublicensees, to practice such non-assigned rights, including, but not limited to, the right to reproduce, modify, display, perform and distribute the Contribution; provided that this license is conditioned upon compliance with Section 2.3.
49 |
50 | (c) To the extent that any of the rights in Section 2.1(a) can neither be assigned nor licensed by You to Us, You irrevocably waive and agree never to assert such rights against Us, any of our successors in interest, or any of our licensees, either direct or indirect; provided that this agreement not to assert is conditioned upon compliance with Section 2.3.
51 |
52 | (d) Upon such transfer of rights to Us, to the maximum extent possible, We immediately grant to You a perpetual, worldwide, non-exclusive, royalty-free, transferable, irrevocable license under such rights covering the Contribution, with rights to sublicense through multiple tiers of sublicensees, to reproduce, modify, display, perform, and distribute the Contribution. The intention of the parties is that this license will be as broad as possible and to provide You with rights as similar as possible to the owner of the rights that You transferred. This license back is limited to the Contribution and does not provide any rights to the Material.
53 |
54 | ### 2.2 Patent License
55 | For patent claims including, without limitation, method, process, and apparatus claims which You [or Your Affiliates] own, control or have the right to grant, now or in the future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with the Material (and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3.
56 |
57 | ### 2.3 Outbound License
58 | As a condition on the grant of rights in Sections 2.1 and 2.2, We agree to license the Contribution only under the terms of the license or licenses which We are using on the Submission Date for the Material or any licenses which are approved by the Open Source Initiative on or after the Effective Date, including both permissive and copyleft licenses, whether or not such licenses are subsequently disapproved (including any right to adopt any future version of a license if permitted).
59 |
60 | ### 2.4 Moral Rights
61 | If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect.
62 |
63 | ### 2.5 Our Rights
64 | You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate.
65 |
66 | ### 2.6 Reservation of Rights
67 | Any rights not expressly [assigned or] licensed under this section are expressly reserved by You.
68 |
69 | ## 3. Agreement
70 | You confirm that:
71 |
72 | (a) You have the legal authority to enter into this Agreement.
73 |
74 | (b) You [or Your Affiliates] own the Copyright and patent claims covering the Contribution which are required to grant the rights under Section 2.
75 |
76 | (c-1) (as Individual) The grant of rights under Section 2 does not violate any grant of rights which You have made to third parties, including Your employer. If You are an employee, You have had Your employer approve this Agreement or sign the Entity version of this document. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement.
77 |
78 | (c-2) (as Entity) The grant of rights under Section 2 does not violate any grant of rights which You or Your Affiliates have made to third parties.
79 |
80 | (d) You have followed the instructions in Section 7.1, if You do not own the Copyright in the entire work of authorship Submitted.
81 |
82 | ## 4. Disclaimer
83 | EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US [AND BY US TO YOU]. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW.
84 |
85 | ## 5. Consequential Damage Waiver
86 | TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU [OR US] BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.
87 |
88 | ## 6. Miscellaneous
89 |
90 | ### 6.1 Accordance
91 | This Agreement will be governed by and construed in accordance with the laws of Finland (European Union) excluding its conflicts of law provisions. Under certain circumstances, the governing law in this section might be superseded by the United Nations Convention on Contracts for the International Sale of Goods ("UN Convention") and the parties intend to avoid the application of the UN Convention to this Agreement and, thus, exclude the application of the UN Convention in its entirety to this Agreement.
92 |
93 | ### 6.2 Overrides
94 | This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings.
95 |
96 | ### 6.3 Third Parties
97 | If You or We assign the rights or obligations received through this Agreement to a third party, as a condition of the assignment, that third party must agree in writing to abide by all the rights and obligations in the Agreement.
98 |
99 | ### 6.4 Failure
100 | The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety.
101 |
102 | ### 6.5 Void
103 | If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law.
104 |
105 | ## 7 Additional Instructions
106 |
107 | ### 7.1 Non-entire copyright ownership
108 | If You do not own the Copyright in the entire work of authorship Submitted:
109 | - Share details about the Copyright situation with Us.
110 | - Wait for explicit approval from Us regarding the copyright situation before submitting your contribution.
111 |
112 | ### 7.2 Acceptance instructions
113 |
114 | - Include the text label `#CLASIGN` in the commit message of your Pull Request.
115 |
116 | By including such label you confirm that You read, understood, and agreed with this Contributor License Agreement, for all your contributions on projects managed by Us. You only have to do this once.
117 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | # License
2 |
3 | ## Creative Commons BY-NC-SA 4.0
4 |
5 | > **Attribution**: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
6 | >
7 | > **Non Commercial**: You may not use the material for commercial purposes.
8 | >
9 | > **Share Alike**: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
10 | >
11 | > **No additional restrictions**: You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
12 |
13 | This is a human-readable summary of (and not a substitute for) the complete license [Creative Commons BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode).
14 |
15 |
16 | ## Safety
17 | Be sure you (and people around you) always use adequate protective equipment and stay safe. Do NOT leave electrical or electronic equipment running without supervision. Do NOT use this project for medical, transportation, weaponry, law enforcement, nor military purposes.
18 |
--------------------------------------------------------------------------------
/preview/print_A.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:99f227ebbaed8061469e76bf46d5e81e88ce5962fe0e27c1d6e387b2fedd48c8
3 | size 324246
4 |
--------------------------------------------------------------------------------
/preview/print_B.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:92a2c43fe684cf4c812da0392a1933c929793f3043b3c525cb014b10a6d1f5a0
3 | size 350691
4 |
--------------------------------------------------------------------------------
/preview/print_C.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:909cd1a9f02334f4d1e5af2284313abc9179fc010abe52bdb4979ebfdb79df93
3 | size 348597
4 |
--------------------------------------------------------------------------------
/preview/print_D.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:cf9a3a3e5ffd08be9a18ac7b7777fe1d6adc91d889bad8988b3788ad13451529
3 | size 318160
4 |
--------------------------------------------------------------------------------
/preview/print_E.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:300e440b0d01514e034eced4319538076ea6c82a361b06a3df8feb5c77cc51da
3 | size 272355
4 |
--------------------------------------------------------------------------------
/preview/print_F.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:c66ed78b23147fbea86e8fba9898060e96f42e79e28c2178c0f8ac88c503edce
3 | size 318010
4 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Alpakka 3D-print
2 |
3 | *Alpakka controller 3D-printed case and buttons (reference designs)*
4 |
5 | ## Project links
6 | - [Alpakka Manual](https://inputlabs.io/devices/alpakka/manual).
7 | - [Alpakka Firmware](https://github.com/inputlabs/alpakka_firmware).
8 | - [Alpakka PCB](https://github.com/inputlabs/alpakka_pcb).
9 | - [Alpakka 3D-print](https://github.com/inputlabs/alpakka_case). _(you are here)_
10 | - [Input Labs Roadmap](https://github.com/orgs/inputlabs/projects/2/views/2).
11 |
12 | ## Previews
13 |
14 |
15 |
16 |
17 |
18 |
19 |
*(Previews might be outdated)*
20 |
21 | ## Dependencies
22 | - Git LFS.
23 | - Blender >= 4.x.
24 | - Blender `bpy` module.
25 | - Build123d python CAD library.
26 | - OCP CAD viewer (VSCode Build123d editor). *[optional]*
27 |
28 | ## LFS and file download
29 | If you only want to download the Blender and STL files `DO NOT USE download ZIP` GitHub button, since it is not compatible with LFS (Large File Storage), but instead get the files from the [latest release](https://github.com/inputlabs/alpakka_case/releases/latest) package.
30 |
31 | To use Git with this project it is required to install Git [Large File Storage](https://git-lfs.github.com).
32 |
33 |
34 | ## Parts hierarchy
35 |
36 | ```
37 | .blend => Blender
38 | .py => Build123d
39 | ```
40 |
41 | ### Main assembly
42 | - `alpakka.blend` - Alpakka controller assembly.
43 |
44 | ### Case
45 | - `case_front.blend` - Front case + cutouts.
46 | - `case_back.blend` - Back case + cutouts.
47 | - `case_cover.py` - Rear bay cover.
48 | - `anchor.blend` - Anchors holding the cases together.
49 | - `lock.blend` - Screws holding the cases together.
50 |
51 | ### Buttons
52 | - `button_dpad.py` - Dpad buttons.
53 | - `button_abxy.py` - ABXY buttons.
54 | - `button_select.py` - Select/start buttons.
55 | - `button_home.blend` - Home button.
56 |
57 | ### Triggers
58 | - `trigger_R1.py` - L1 and R1 shoulder triggers.
59 | - `trigger_R2.blend` - L2 and R2 triggers.
60 | - `trigger_R4.blend` - L4 and R4 triggers.
61 |
62 | ### Control widgets
63 | - `hexagon.py` - Touch sensitive surface.
64 | - `thumbstick.blend` - Left thumbstick cap.
65 | - `thumbstick_right.py` - Right thumbstick cap.
66 | - `scrollwheel.py` - Scrollwheel, core, and holder.
67 |
68 | ### Additional
69 | - `soldering_stand.blend` - Tool to hold the PCB while soldering.
70 | - `shared.blend` - Common geometry nodes / modifiers shared by all parts.
71 |
72 |
73 | ## Exported filename labels
74 | - `015mm`: 0.15mm layer height, default for most prints.
75 | - `007mm`: 0.07mm layer height, for parts that require extra finesse.
76 | - `020mm`: 0.20mm layer height, for tools that we want to print fast.
77 | - `2x`, `4x`: To be printed multiple times.
78 | - `CONDUCTIVE`: Electrically conductive filament.
79 |
80 | It is very recommended to follow these indications, and to check the [Manual](https://inputlabs.io/devices/alpakka/manual/diy_case) for more details.
81 |
82 |
83 | ## Migration to Build123d
84 | We are in the process of migrating 3D modelling from Blender to [Build123D](https://build123d.readthedocs.io), we decided to make the migration gradually, one part at a time.
85 |
86 | The original Blender parts are still located in `blender/` folder.
87 |
88 | While parts that are already ported into Build123D are located in `build123d/` folder.
89 |
90 | The export script will create `STL` for all Blender and Build123D parts, and `STEP` only for Build123d parts.
91 |
92 |
93 | ## Developer commands
94 | - `make release` - Create `blender.zip`, `stl.zip` and `step.zip`.
95 | - `make clean` - Remove all export files and leftovers.
96 | - `make blend` - Export only Blender files.
97 | - `make b123d` - Export only Build123d files.
98 |
--------------------------------------------------------------------------------
/scripts/export_b123d.py:
--------------------------------------------------------------------------------
1 | from build123d import Axis, Plane, Compound, Location, export_stl, export_step
2 |
3 | # Import parts.
4 | import sys
5 | sys.path.insert(1, './build123d')
6 | from button_dpad import button_dpad
7 | from button_abxy import button_abxy
8 | from button_select import button_select
9 | from thumbstick_right import thumbstick_right
10 | from wheel import wheel_default, wheel_loose, wheel_tight
11 | from wheel_core import core
12 | from wheel_holder import holder
13 | from trigger_r1 import trigger_r1
14 | from cover import cover
15 | from hexagon import chex
16 | from dongle_case import dongle_case
17 |
18 | STL_DIR = 'stl/'
19 | STEP_DIR = 'step/'
20 |
21 | def export(obj, filename, subdir=''):
22 | print(f'Exporting {subdir}{filename}')
23 | export_stl(obj, STL_DIR + subdir + filename + '.stl')
24 | export_step(obj, STEP_DIR + subdir + filename + '.step')
25 |
26 | # Buttons.
27 | export(button_abxy.part, '007mm_abxy_4x')
28 | export(button_dpad.part, '007mm_dpad_4x')
29 | export(button_select.part, '007mm_select_4x')
30 |
31 | # Trigger L1/R1.
32 | export(trigger_r1.part, '015mm_trigger_R1')
33 | export(trigger_r1.part.mirror(Plane.YZ), '015mm_trigger_L1')
34 |
35 | # Scroll wheel.
36 | export(wheel_default.part, '015mm_wheel')
37 | export(wheel_loose.part, '015mm_wheel_loose', 'variants/')
38 | export(wheel_tight.part, '015mm_wheel_tight', 'variants/')
39 | export(core.part.rotate(Axis.X, 90), '007mm_wheel_core')
40 | export(holder.part, '015mm_wheel_holder')
41 |
42 | # Battery Cover.
43 | export(cover.part, '015mm_cover')
44 |
45 | # Conductive Hex.
46 | export(chex.part, '015mm_hexagon_CONDUCTIVE')
47 |
48 | # Thumbstick right.
49 | export(thumbstick_right.part, '007mm_thumbstick_R')
50 |
51 | # Dongle case.
52 | export(dongle_case.part, '015mm_dongle_case')
53 |
--------------------------------------------------------------------------------
/scripts/export_blender.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import math
3 | from pathlib import Path
4 |
5 | context = bpy.context
6 | scene = context.scene
7 | viewlayer = context.view_layer
8 |
9 | class Entry:
10 | def __init__(
11 | self,
12 | col_name,
13 | stl_name,
14 | axis_up='Z',
15 | axis_forward='Y',
16 | merge=False,
17 | split=False,
18 | multiple=False,
19 | rotate=False,
20 | tolerance=False,
21 | mirror=False,
22 | ):
23 | self.col_name = col_name # Collection name.
24 | self.stl_name_base = stl_name
25 | self.stl_name = stl_name
26 | self.axis_up = axis_up
27 | self.axis_forward = axis_forward
28 | self.merge = merge # Export all the objects in the collection merged.
29 | self.multiple = multiple # Multiple individual exports in the collection.
30 | self.split = split
31 | self.rotate = rotate
32 | self.tolerance = tolerance
33 | self.tight = tolerance # Export tight variant.
34 | self.loose = tolerance # Export loose variant.
35 | self.mirror = mirror
36 |
37 | @staticmethod
38 | def find(name):
39 | for entry in entries:
40 | if entry.col_name == name:
41 | return entry
42 |
43 | @property
44 | def objects(self):
45 | return [o for o in scene.objects if o.type == 'MESH']
46 |
47 | def process(self, variant=False):
48 | bpy.ops.object.select_all(action='DESELECT')
49 | if not variant and self.tolerance:
50 | if self.tight:
51 | self.process_tight()
52 | if self.loose:
53 | self.process_loose()
54 | for ob in self.objects:
55 | if not ob.visible_get(): continue
56 | viewlayer.objects.active = ob
57 | ob.select_set(True)
58 | if self.rotate:
59 | self.do_rotate(ob)
60 | if self.split:
61 | self.do_split(ob)
62 | if not self.merge:
63 | if self.multiple:
64 | subentry = Entry.find(ob.name)
65 | subentry.export()
66 | else:
67 | self.export()
68 | ob.select_set(False)
69 | if self.merge:
70 | self.export()
71 | if not variant and self.mirror:
72 | self.process_mirror()
73 |
74 | def do_rotate(self, ob):
75 | for mod in ob.modifiers:
76 | if 'rotation' in mod.name:
77 | mod.show_viewport = True
78 |
79 | def do_split(self, ob):
80 | for mod in ob.modifiers:
81 | if 'instance' in mod.name:
82 | mod.show_viewport = False
83 |
84 | def process_tight(self):
85 | for ob in self.objects:
86 | for mod in ob.modifiers:
87 | if 'loose' in mod.name:
88 | mod.show_viewport = False
89 | if 'tight' in mod.name:
90 | mod.show_viewport = True
91 | self.stl_name = f'{entry.stl_name_base}_tight'
92 | self.tight = False
93 | self.process(True)
94 |
95 | def process_loose(self):
96 | for ob in self.objects:
97 | for mod in ob.modifiers:
98 | if 'loose' in mod.name:
99 | mod.show_viewport = True
100 | if 'tight' in mod.name:
101 | mod.show_viewport = False
102 | self.stl_name = f'{entry.stl_name_base}_loose'
103 | self.loose = False
104 | self.process(True)
105 |
106 | def process_mirror(self):
107 | for ob in self.objects:
108 | for mod in ob.modifiers:
109 | if 'Mirror print' in mod.name:
110 | mod.show_viewport = True
111 | self.stl_name = self.stl_name.replace('R', 'L')
112 | self.mirror = False
113 | self.process(True)
114 |
115 | def export(self):
116 | root = Path(bpy.path.abspath("//")).parent
117 | path = root / 'stl' / f'{self.stl_name}.stl'
118 | bpy.ops.export_mesh.stl(
119 | filepath=str(path),
120 | use_selection=True,
121 | axis_up=self.axis_up,
122 | axis_forward=self.axis_forward)
123 |
124 |
125 | entries = [
126 | Entry('Case front', '015mm_front'),
127 | Entry('R1', '015mm_trigger_R1', split=True, mirror=True),
128 | Entry('R2', '015mm_trigger_R2', '-Z', split=True, mirror=True),
129 | Entry('R4', '015mm_trigger_R4', '-Y', '-Z', split=True, mirror=True),
130 | Entry('DHat', '015mm_dhat'),
131 | Entry('Case back', '015mm_back', '-Z'),
132 | Entry('Home', '007mm_home', rotate=True),
133 | Entry('Thumbstick', '007mm_thumbstick_L', tolerance=True),
134 | Entry('Anchor', '015mm_anchors_2x', '-Z', split=True),
135 | Entry('Soldering helper', '020mm_solderstand', '-Z', merge=True),
136 | ]
137 |
138 | for collection in bpy.data.collections:
139 | entry = Entry.find(collection.name)
140 | entry.process()
141 | break
142 |
--------------------------------------------------------------------------------