├── .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 | --------------------------------------------------------------------------------