Skip to main content

3D Enclosure Design

Parametric handheld console enclosure designed in OpenSCAD, optimized for FDM 3D printing.

Design Overview​

The enclosure follows a landscape form factor inspired by the Game Boy Advance and Nintendo Switch Lite.

Dimensions: 165.8 × 80.8 × 26.6 mm — V3.1 (2026-11-01)

Source files

The OpenSCAD project files are in hardware/enclosure/. Open with OpenSCAD 2021.01+ (the Docker image runs the 2026 dev build).

make render-enclosure                # 15 PNG views -> website/static/img/renders/enclosure/
make export-enclosure-stl # print set -> 3d_case/, viewer set -> 3d_case/viewer/
make generate-enclosure-pcb # pcb_parts.scad from board.py (also run by generate-pcb)
make verify-enclosure-sync # scad constants vs board.py / datasheets (34 checks)
make verify-enclosure-requirements # the user's constraints (20 checks: labels, thin walls, ...)
make verify-enclosure-collision # CGAL interference: shells vs PCB/panel/battery/caps
make verify-enclosure-stl # measure the exported STLs themselves (S0-S14)

V3.1 — after printing V3 (2026-11-01)​

The printed V3 failed as a case (failure record in the V3 section below): buttons wobbled, parts were misaligned, the L/R mechanism did not work and the power slider broke at once. The user's asks:

AskWhat changedGate
"The board must sit in a frame, with no ambiguity on its position, so the screw axes are obvious"Inner wall face 0.3 mm from the PCB edge on every side (pcb_gap); the wall keeps its 2.6 mm, so the outline becomes 165.8 × 80.8 (board + 2 × 2.9) and the corners r 8.9, concentric with the PCB's r 6 (an 11 mm corner would leave 1.7 mm of wall on the diagonal). The 4 corner screws are the only screwssync body-margin, STL S1 (the 0.3 gap measured on all four sides), R15
"The parts round the edge closer: USB, power switch and micro-SD"The tighter outline brings the J1 mouth to 3.1 mm inside the face (was 5.2), the SD card end to 2.1 (was 4.2), the SW16 knob to 3.0 (was 5.1)sync sd-slit, usbc-recess, pwr-window-vs-sw16
"USB: no mouth, only the space of the USB-C — as big as the USB-C — closer, and reinforced (a kind of cage)"The 17 × 9 recess is gone; the hole is the USB-C plug's own size: shell 8.25 × 2.4 + clearance = 9.0 × 3.2, on the J1 axis, ending at the shell split (the lip above stays whole). The cable's overmold seats on the face: J1's mouth is 3.07 mm inside it, within the 6.5 mm the USB-IF locking-connector spec allows between the receptacle datum and the overmold seat. The 2.6 mm wall round the hole is the collar; inside, a cage round J1 (cradle 0.3 under its body, a 1.2 mm cheek 0.3 off each side; no back wall: J3 and R1/R2 sit behind it). An earlier 13 × 7.1 overmold-sized hole left a 0.05 mm strip of the lip across it, unprintable on the MJPsync usbc-recess, R13, STL S8
"The power slider broke at once (weak toward the PCB lever) — keep a slider, but reinforced"A solid shuttle: a 10 mm core slides in a 12 mm window (sill Z 12, open to the split), a 16 mm flange inside keeps it in (1.25 thick beside SW16, a 3.75 thick block under it) (1.2 mm over the wall at full travel; the PCB, 0.2 above, keeps it down), and the knob sits in a 2 mm notch cut through that flange — 7 mm of solid material each side of the knob instead of the V3's two 1.2 mm fork arms. An 8 × 3 nub stands 1.2 proud for the fingersync pwr-slider-vs-sw16, R8, R13, STL S8, S10
"Pressed, the D-pad goes off axis"Its pivot ball rested free on the bare PCB. A printed centring cup (Ø6 × 1, 3d_case/dpad_cup.stl) taped to the PCB at the D-pad centre holds the ball in a dimple: the cross still rocks, it can no longer slide. The pivot is 0.45 mm shorterR18
"Assembly is top shell first, upside down: display, caps, then the PCB — it must sit on the screw axes"At the PCB plane the top shell is only its outer skin (the lip groove is there), so the board had 1.7 mm of play. Six fences come down from the ceiling to the PCB bottom face 0.1 mm off its edge (+Y at x ±40, −Y at x −15/+30, ±X at y 15, clear of ports, wells and bosses); the bottom shell's lip is notched where they passR19
"L and R back to the previous solution"The V2.1 hinged levers on the back face are back (face 14 × 8, hinge rod printed with the shell, nub on SW11/SW12 at Z 14.3); the back fillet drops to 3 mm so the faces sit on the flat; "L"/"R" engraved beside the levers toward the centresync shoulders, lever-counterbore, lever-clear; R14, R15; STL S8, S10
"The caps stay 2 mm out but must have no lateral play, always on axis"Guide ribs on every face cap but the D-pad (which rocks on its pivot): narrow vertical ribs over the guided length come within btn_rib_gap = 0.05 mm of the bore — tilt 1.6° instead of 6.8°. A negative gap makes them crush ribsR17
"New battery: the 103665 Li-Po (3.7 V, 3000 mAh, PH2.0) the user bought, on the other side from the speaker grille"Pocket 70 × 38 × 10 for the 65 × 36 × 10 cell at x 3…73 (away from the speaker), leads out of the −X end toward J3; straps at x 18 / 60, pegged on both sides. A 65 × 40 × 12 cell was tried first: 12 mm reaches Z 14, above the ESP32's underside (12.9) — it does not fit without a deeper bottom shell, which the user ruled outR10, STL S7; sync battery-pocket against BT1, now the 103665 model (DNK family page in hardware/datasheets/)

The print set loses lr_cap_x2 and lr_lever_l/r; it gains lever_l.stl / lever_r.stl and dpad_cup.stl; power_slider.stl is the new reinforced shuttle. 3d_case/merged.stl is one plate with every printed part except the two shells (caps, D-pad, Start/Select/Menu, levers, slider, D-pad cup, two straps), laid out in rows of at most 120 mm (114 × 99 mm), rebuilt by make export-enclosure-stl (scripts/build_print_plate.py).

V3.1 design review (2026-11-03)​

A review of V3.1 before printing it, against what the gates cannot see (they check the design against the numbers we chose, not whether the numbers are right):

ProblemChangeGate
The six PCB fences were 0.1 mm off the board edge, but JLCPCB routes the outline to ±0.2 mm: a board cut oversize could jampcb_fence_gap 0.1 → 0.25 (outline tolerance + 0.05); the M2.5 screws pull the board onto the insert axesR19 now requires 0.25…0.35; mutation Q33 (0.1) must fail
Guide ribs 1.0 mm wide, 0.05 mm off a printed bore: a rib that prints a tenth proud binds and the button sticks pressedRibs 0.6 mm wide, so a proud rib shaves in instead of jamming; the gap stays 0.05 (tilt 1.6°, within the 2° asked — 0.1 would rock 3.3°)R17
The cell had zero room in Z: 10 mm pocket, 10 mm cell, strap on top, 0.9 mm under the ESP32. Pouch cells come a few tenths thick and swellPocket floor sunk 0.7 mm into the 2 mm floor (1.3 left under the cell): 10.7 mm floor-to-strap. A 10.0 cell takes a foam pad under itR10 (headroom ≥ 6 %), R8 (floor ≥ 1.2); mutations Q34, Q35
The caps were exported as they sit in the assembly — on the Ø3 stem with the flange in the air, the D-pad on the tip of its pivot ball (0.2 mm² on the bed). Fine on MJP (wax supports everything), unprintable on FDM, and the opposite of this page's print tablepart_*_print selectors turn the eight face caps over about X: the face is on the bed (23 mm² for A/B/X/Y, 113 mm² for the D-pad)S0 (each print file is a pure rotation of its assembly copy)
The plate was one row 252 mm long, past a 220 or 250 mm bedShelf packing, rows ≤ 120 mm: 115 × 99 mmS14
"The buttons must go down straight": the ribs only guided 3.5 mm (front wall + half the well) of an 8.4 mm cap — at a printed 0.15 mm gap it still rocks 4.9°Piston caps: A/B/X/Y, Start, Select and Menu carry a solid piston (body + 1.2) under the front wall, sliding in a tube that comes down from the ceiling to Z 19.2 (1.6 over the PCB). Guided at the face (front-wall ribs) and at the piston (piston ribs, 0.05 off the tube bore): guide base 6.5 mm, tilt 0.9° nominal, 2.6° at a printed 0.15 gap. The switch is 1.5 tall, so the piston (bottom Z 20.0) stays above it even fully pressed: no hollow needed, and the pills, narrower than the switch, get one too. Stop = a stepped 45° shoulder whose widest step meets the ceiling. Y/Select: piston and tube clipped on the glass side (the tube is a C). Menu: the tube is slotted under LED3-6. D-pad unchanged (it rocks on its pivot)R17 (tilt ≤ 2° nominal and ≤ 3° at +0.1 print gap; piston stays in the tube and above the switch pressed); mutations Q36, Q37. A PCB respin can do better still: a taller switch with a big plunger the cap fits over
Gate S13 sampled the shortened back-right boss rib at 6.1 mm from the axis, past its own 5.0 mm tip: it passed only because the old Menu well filled that pointSample at the rib's real reach (5.0 − 3.6 = 1.4 from the boss surface, radius 4.3)S13

V3 — the user's review of the V2.1 shell (2026-10-07) — FAILED when printed​

V3 failed as a case

V3 was printed (MJP, 0.3 mm) in October 2026 and did not work as an enclosure. It is superseded by V3.1 above; do not print the V3 files. What failed, why, and what replaced it:

Failure on the printCauseV3.1 answer
Buttons wobbled (V2.1 caps were "perfect")The only cap change V2.1 → V3 was btn_face_h 0.6 → 2.0: the same well clearance now steers a cap that sticks out 2 mm, so it rocks 6.8°Guide ribs 0.05 mm off the bore: 1.6°
Parts misaligned, the PCB had no defined position~1.7 mm of play between board and shell at the PCB plane; nothing located the board on the screw axesWall 0.3 mm off the PCB on all sides + six fences 0.1 mm off its edge in the top shell (0.25 since the 2026-11-03 review)
L/R bell-crank mechanism did not workNot diagnosed on the print. The chain cap → bell crank → switch has two pivots and a sliding contact in printed plastic for 0.45 mm of switch travel; the user chose to go back rather than debug itV2.1 hinged levers on the back face, restored
Power slider broke at onceTwo 1.2 mm fork arms round the SW16 knob, the weak point toward the PCB leverSolid shuttle, 7 mm of material each side of the knob
D-pad went off axis when pressedPivot ball free on the bare PCBPrinted centring cup under the pivot
USB-C recess too far from the connector, thin bridge over it17 × 9 pocket round a 13 × 6.5 hole, 5.2 mm from J1's mouthPlug-sized hole 9.0 × 3.2 ending at the split, J1 caged

The V3 asks below are kept as the record of what was tried.

Five asks, each now a named constant with a gate (R13-R15 in verify_enclosure_requirements.py, S8/S10 in verify_enclosure_stl.py):

AskWhat changedNumbers
"The buttons must stand about 2 mm out of the face so they can be found by touch, with rounded edges"btn_face_h 0.6 → 2.0, every cap head rounded with btn_face_r 1.5 (rounded_top_extrude: stacked inward offsets on a quarter circle)cap 7.9 → 9.3 tall; the stem, flange and well are unchanged
"L/R turned 90° toward Z, on the edge between the two covers, 2-3 mm out" — then (2026-10-08) "I keep SW11/SW12 soldered: a printed mechanism from the caps to the switches"SW11/SW12 stay on the board (TS-1187A, PCB bottom side, actuator facing −Z). A 14 × 8 cap slides through a window in the +Y wall whose top edge is the shell split, 2.5 mm proud; behind it a printed bell-crank lever per side turns the cap's −Y push into a +Z push on the switchlever pivot: Ø2 pins along X at (y 37.6, Z 13.3) in two U-slot blocks fused to the inner wall; lower arm with a bump 0.05 mm behind the cap flange at Z 7.7; flat upper arm (top Z 14.3 = actuator tip − 0.2) whose end edge sits on the switch centre line. Arms 5.6 / 5.6: cap travel 0.45 and 160 gf, like the face buttons
"The speaker on the left"Player's left = the D-pad side (−X), where board.py also marks SPEAKER_ENC; the battery pocket slides +2.6 mm in X to free the Ø32 seatgrille at (−62.5, 8); pocket x −45…50
"Rounded like the Switch Lite"Both shells are hulls of fillet slices (filleted_prism / filleted_cavity): plan corners 8 → 11, an 8 mm fillet all round the back face, a 2 mm fillet round the frontthe floor's inner outline is inset 8 mm: the speaker driver (edge x −76.5) stays inside it; the front fillet leaves 1.40 mm at the inner corner
"The bottom-edge openings recessed into the face: USB ≥ 2 mm, SD ≥ 4 mm, the switch computed so SW16 can really be moved"USB-C: a 17 × 9 pocket 2 mm deep round the 13 × 6.5 opening, with a 1.6 mm pad on the inner wall face so the floor stays 2.2 thick. SD: the V2.1 16 × 3.5 slit through the bottom-shell wall at Z 14.2 (the V3 20 × 4.7 window was reverted by the user on 2026-10-08); the TF-01A housing front is aty

The KSS341GLFS (C&K, C221798 — IP40, 100k, 4 N) is the right on-board side switch for the next PCB respin, with its own footprint at the edge; it cannot sit on the TS-1187A pads (two 1.7 mm feet 7.6 apart, pushing perpendicular to them — the pad pattern's long axis points at the edge). Datasheet kept in hardware/datasheets/SWLR-RESPIN_KSS341GLFS_C221798.pdf.

V2 — what the first printed shell taught us​

The V1 shell (printed April 2026) was designed before the panel, the tactile switches and the JST connector were measured. Everything below is a consequence of a real dimension, and every dimension is now a named constant that scripts/verify_enclosure_sync.py checks against board.py or a datasheet.

V1 problem on the printed shellRoot causeV2 fix
Display did not fit / viewport wrong sizeV1 viewport was 86.4 × 64.8 (a guessed 4.0" active area). The real ILI9488 3.95" panel with touch is 94.57 × 60.88 × 3.90 mm outline, 83.52 × 55.68 active, with the 8-9 mm driver-ledge border on the D-pad sideViewport = active area. The glass is centred between the Select cap and the Y cap bodies (0.47 mm gap each side — those two caps have their flange clipped on the glass side); the active area lands at x = 3.7, y = 2. The glass may overhang the FPC slot
No room for the FPC tail + extension boardThe R37 workaround chain (panel tail → 40P extension board → type-B FFC → slot → J4) runs under the panel, over the PCBTail on the D-pad side, U-folded under the glass (1.8 mm fold zone, 2.1 mm from SW4); extension board 28 × 24 × 3 placed under the glass before the slot (x 16.5…44.5); 3.1 mm cable riser → top interior exactly 7.0 mm above the PCB (disp_stack)
Nothing held the panelV1 had a bezel but no frame; the panel floated on the PCBTop shell grows a display frame: 1.2 mm rim walls from the ceiling down to the PCB along both long sides plus two stubs beside the FFC passage; open on the tail side. The panel is taped to the bezel from inside; the frame contains it and lands on the PCB plane when the screws are tightened
No screw bosses in the top shellV1 screws threaded into the bottom shell's own bosses — the top shell was never fastenedTop-shell bosses Ø7.2 at the 4 PCB corner holes, 7 mm tall (they are the spacer to the PCB), with a socket Ø3.1 × 4.5 for an M2.5 heat-set insert HANGLIFE M2.5 × D3.5 × L4 (OD 3.5, L 4.0) at the PCB end, 2 mm of plastic around it (boss Ø7.2) and a Ø2.8 relief above it (plain cylinders: the insert is pressed from the PCB side and fuses into the boss wall). M2.5 × 20 from the back, through the bottom column and the PCB hole, into the insert
Bottom bosses hit the L/R buttonsThe corner hole (70, 30.5) is 5 mm from switch SW11/SW12 at (65, 32). A plunger centred on the switch can be at most ~3 mm wide next to the counterboreL/R are hinged levers: pivot rod printed in the shell at x = 51 (outside the 95 mm battery pocket), nub on the switch at x = 65, face 14 × 8.5 mm ending 0.45 mm before the counterbore. Bottom columns get a Ø4.4 neck under the PCB with a relief on the switch side and four 1.2 mm gussets from the floor (outward and along ±Y, none toward the levers)
Button caps could not reach the switchesV1 stems were 2 mm with a 6.4 mm interior; real switch height is 1.5 mmCap stack computed from the real numbers (table below): 3 mm guide well under the ceiling, countersunk conical flange, 1.5 mm stem — 7.9 mm total. Sizes up: ABXY Ø9, D-pad arms 6.5, Start/Select 10 × 5, Menu 12 × 4 — Start/Select cannot grow in X and ABXY cannot pass Ø9 without moving a switch (the glass is between them)
Battery pocket wrong size and placeV1 pocket 85 × 50 sat over J3 (JST S2B-PH-SM4-TB, 5.5 mm tall under the PCB, datasheet p.4); the fitted cell measures 90 × 50 × 10 (user, 2026-09-16), not 80Pocket 95 × 50 × 10 at x −49…46, y −20…30; border 8 mm (below J3's 10.5 mm underside); corners notched for the hinges; clips removed (a pouch cell is held by a foam pad, not pinched). Gates: pocket ≥ measured cell (R10) and clear of all 77 bottom-side parts
Speaker grille in the wrong cornerV1 grille bottom-right when looking at the backGrille top-left when looking at the back = enclosure (+63, +8); driver seat clears the pocket border (x 47.5), the R lever hinge, the side rib and the column (23.6 mm)
Only 2 of 6 LEDs visibleLED3-LED6 (bring-up diagnostics) had no light pipesØ1.2 light pipes at (54…72, −20.5); where they cross the Menu guide-well ring the ring is opened outward (no web thinner than 1.2 mm)
USB-C plug could not seatReceptacle mouth is at the PCB edge, 5 mm inside the wall; the 9 × 3.2 hole only passed the plug shellOpening sized for the plug overmold (13 × 6.5), crossing the shell split
Battery free to lift against the PCBNothing held the cell down; the ESP32 underside is only 0.9 mm above the cell topTwo printed hold-down straps 5 × 1 mm over the cell at x = −30 and +26 (outside the module footprint), pegged into four posts on the pocket border at the cell plane (Z 12); the PCB captures them. Gate: straps and posts ≥ 0.5 mm under every bottom-side part
Print service rejected the STLs: "thin walls"Groove skin 0.7, column necks 0.8, speaker ring 0.7, tongue 1.0, cap flanges 0.8, straps 1.0, lever hook 0.35One constant min_wall = 1.2: side walls 2.6 (1.2 skin + 1.2 tongue + 0.2), column contact = outer half-column (no neck), speaker ring 1.7, cap flange 1.2, straps 1.2, lever hook 5 mm wide with 1.35 walls, well rings trimmed through their bore beside the glass, LED pipes open the Menu ring outward, port cutouts only through the wall. Gate R8 tabulates 17 named walls; R12 slices every shell and lever in OpenSCAD, erodes by 0.6 and fails on anything that disappears
Bosses and frame 0.4 mm short of the PCB (found 2026-09-17 by measuring the STL)The lip groove was subtracted as a full slab, chopping the last 0.4 mm off every internal feature — invisible to constant-based gatesGroove is a ring; new gate verify_enclosure_stl slices the exported 3d_case/*.stl with OpenSCAD import() and measures every cutout, boss, pocket and part against board.py, the datasheets and the requirements — a second, independent road (S0–S14)
Hand-drawn PCB modelA dozen boxes from memory; J3 was 3 mm too short, LEDs missinghardware/enclosure/pcb_parts.scad is generated from board.py (all 98 fitted parts with body sizes/heights, slot, holes); a stale file is a red gate
Engraved labels wrongV2.0 mirrored the whole label group about x = 0 (A/B/X/Y landed on the D-pad); V2.1 mirrored every top glyph (backwards B, STA/SEL)Top-shell glyphs are engraved as-is (read from the front), only the back-face L/R are mirrored about their own centre; gate R9 exports the glyphs and checks position and chirality (stem side of the B and the L)

Z-axis stack (closed assembly, Z = 0 at the back face)​

Z (mm)What
0Back face
2Floor inner face — battery pocket, speaker seat, L/R lever pivot blocks
13.3L/R lever pivot axis (pin Ø2, top flush with the lever top plane Z 14.3); cap window Z 8 – 16, cap axis Z 12
2 – 12Battery cell (90 × 50 × 10, measured)
10.5 – 16J3 JST (5.5 mm under the PCB) — the pocket never overlaps it in XY
12.9 – 16ESP32 module (3.1 mm), 0.9 mm above the cell
16PCB bottom face = shell split = bottom column tops (pcb_z)
17.6PCB top face; face switches to 19.1 (TS-1187A code A, 1.5 mm)
17.6 – 20.7Cable riser (3.1): folded tail, extension board, type-B FFC
20.7 – 24.6Panel (3.9 mm, touch version)
24.6Ceiling (front wall inner face) = glass top
26.6Front face

Button caps — computed from the stack​

All face caps share one stack; the numbers are echo()ed by OpenSCAD on every render and checked by the sync gate (cap-stem).

ElementHeightConstant
Face proud of the front surface2.0 (rounded, r 1.5)btn_face_h, btn_face_r
Body through the wall2.0wall
Body through the guide well under the ceiling3.0btn_guide_h
Flange plate (outside the well bore)1.2btn_flange_h
Stem to the switch actuator1.1btn_stem_h = 7.0 − 3.0 − 1.2 − 1.5 − 0.2
Total cap height9.3btn_cap_h

That stack is now the D-pad's only. A/B/X/Y, Start, Select and Menu are piston caps (2026-11-03), same 9.3 total:

ElementZConstant
Face top28.6btn_face_h above the front face
Body through the front wall (ribs 0.05 off the hole)24.6 – 26.6wall
Stepped 45° shoulder, widest step meets the ceiling (stop)23.64 – 24.84z_shoulder0, btn_piston_steps
Piston, body + 1.2, ribs 0.05 off the tube bore20.0 – 23.64z_piston0, btn_piston_extra
Stem Ø3 to the plunger (0.2 pre-travel)19.3 – 20.0btn_piston_stem_h
Tube in the top shell, bore = piston + 0.3, wall 1.219.2 – 24.6guide_tube(), z_tube0, btn_tube_t
  • Cap body = cutout outline − 0.6 mm (0.3 per side). Flange = body + 1.2 mm radial, 1.2 mm plate; its top 1.5 mm is a stepped 45° cone that seats in a matching countersink at the well's end — self-centring and printable face-down without supports.
  • Stem Ø3.0 (switch plunger Ø2.0). 0.2 mm pre-travel gap, 0.25 mm switch travel.
  • The D-pad additionally has a Ø3 centre pivot resting on the PCB (3.2 mm tall) so the cross rocks instead of sinking.
  • Guide ribs (V3.1, piston since 2026-11-03 — see the table above; the text below is the V3.1 record): every face cap but the D-pad carries narrow vertical ribs (1.0 wide, 0.6 since the 2026-11-03 review; ABXY at 45°/135°, the pills on both long sides at ±w/4 and on both ends) over the guided length — from above the countersink to the front face, 3.5 mm. They stop btn_rib_gap = 0.05 mm from the bore, so a cap can tilt 1.6° instead of 6.8° on its 0.3 mm/side body clearance. Set the gap to 0 or below for crush ribs. The D-pad has none: it must rock on its pivot.
  • L/R levers (V3.1, the V2.1 design): one hinged lever per side on the back face (L is the mirror of R, so two files). Face 14 × 8 standing 1 mm out of the back through a 14.8 × 8.8 cutout, a 1.2 mm flange inside the floor (1 mm wider), a Ø3.4 nub up to Z 14.3 (actuator tip − 0.2) on SW11/SW12 at (±65, 32), a 5 mm tongue hooking over a Ø2 rod printed with the shell at x = ±50.4. Prints face-down as it sits: 3d_case/lever_l.stl, 3d_case/lever_r.stl.
  • Power slider (V3.1, reinforced): core 10 wide through a 12 mm window at x = −40 (sill Z 12, open to the split), flange 16 wide behind the wall (1.25 beside SW16, 3.75 under it) with a 2 mm notch for the MSK12C02 knob (1.6 wide, 1.6 travel, 3.0 mm inside the face), nub 8 × 3 × 1.2 proud. 3d_case/power_slider.stl, printed on the lower block's inner face, nub up.
  • Brand lines (2026-10-08): "GAME BRO!" engraved 0.6 deep above the screen on the top cover (4.5 mm, y = 36.3) and "CPJ & CP 2026" low on the back face (3 mm, y = −27, mirrored for reading from the back); gate R9 counts their glyphs and checks the chirality of the B and the J.

Screws and inserts​

  • 4 × M2.5 heat-set inserts HANGLIFE M2.5 × D3.5 × L4 (OD 3.5, L 4.0), pressed into the top bosses from the PCB side (socket Ø3.1 × 4.5; boss Ø7.2 = 2.05 mm wall).
  • 4 × M2.5 × 20 pan head from the back, counterbore Ø5.0 × 1.8. Path: floor → bottom column (Ø6, Ø2.8 bore, outer half-column for the last 2 mm so the contact face clears SW11/SW12, four 1.5 mm gussets) → PCB Ø2.5 hole → insert (Z 17.6…21.6) → tip at Z 21.8, 0.2 mm into the Ø2.8 relief (Z 22.1…24.1). A 25 mm screw would hit the roof: the gate R6 screw-length rejects it.
  • The two centre PCB holes (±25, 0) are under the battery pocket and stay unused.

Rendered Views​

Front (Display Side)​

Front View

Plan view​

Top View

Back — 3 mm fillet, L/R lever faces, speaker grille (player's left), screw counterbores​

Back View

Bottom edge — USB-C overmold opening (no recess), SD slit, power-switch window in its trough​

Ports

L/R lever (R side) cut through SW12 — face in the back, hinge rod, nub under the switch​

L/R lever

Power slider section

Every printed part, as it comes off the bed​

Parts

Exploded View​

Exploded View

Cross-section, XZ at Y = 0 — battery, module, PCB, panel + riser, caps​

Cross-Section View

Cross-section, YZ through the Y button — panel pocket, tail fold, guide well​

Cross-Section YZ

Top shell from the inside — display frame, screw bosses, guide wells​

Top inside

Same, bare — bosses with the four inserts seated, LED light pipes through the Menu well​

Top inside bare

The groove bug the STL audit caught (section through a boss, before / after)​

Groove bug

Exact 2D sections at y = 30.5 (through the top-right boss), old model on the left. The lip groove was subtracted as a full 167.6 × 82.6 slab from Z 18.0 up: every boss and frame wall stopped at Z 18.0, 0.4 mm above the PCB top (17.6), and the insert stuck out of its socket. The constants said "7.0 mm interior"; only measuring the exported STL (gate S5) showed the geometry did not. The groove is now the ring between the outer skin and the interior.

One boss cut through its axis — the print geometry of the insert socket​

Boss section

From the PCB-side face of the boss inward: socket Ø3.1 × 4.5 deep for the insert (HANGLIFE M2.5 × D3.5 × L4: OD 3.5, L 4.0, knurled — pressed with a soldering iron, flush with the boss face), then a Ø2.8 relief 2.0 deep for the screw tip, then 0.5 mm of boss plus the 2 mm front wall. Boss Ø7.2 = 2.05 mm of plastic around the socket (the user asked for 2). M2.5 × 20 tip at Z 21.8 = 0.2 mm past the insert end: full 4 mm engagement. Gate R6 pins these numbers.

Bottom shell without the PCB — 103665 pocket with hold-down straps, columns with gussets, ribs, L/R levers on their hinges, USB cradle, speaker seat ring​

Bottom inside

Fit Check (bottom shell + PCB + battery + L/R levers)​

Fit Check View

Interactive 3D Viewer​

tip

Open the interactive 3D viewer — three views: Assembly, Exploded, and Parts, which lays every piece out side by side as it sits on the print bed (the inner ones too: L/R levers, power slider, D-pad cup, straps). Each part in the list has a show/hide box and a Solo button that shows only that part and frames it; Show all brings everything back. The viewer loads the print files themselves (3d_case/*.stl, served through staticDirectories) and only rotates and moves them into place (3d_case/viewer/placement.json), so what you see is exactly what you print. Only the display and the PCB, which are not printed, come from 3d_case/viewer/parts/. make export-enclosure-stl regenerates everything; gate S0 of verify_enclosure_stl.py proves each placed print file lands exactly on the design's assembly position with a pure rotation (never a mirror). No STL lives under website/.

The Parts & colours panel lists every placed part. A colour picker repaints it live, a checkbox hides it, and clicking a part in the 3D view selects its row. Colours are kept in the browser (localStorage) and survive the Assembly/Exploded switch. Copy puts the whole colour set on the clipboard as JSON and Paste applies one. The print files are never touched.

Features​

Front Panel (top shell)​

  • Display viewport 83.52 × 55.68 (+0.3 clearance) at (3.7, 2) — the panel's active area, cosmetic 2 mm raised bezel; glass pocket x −46.8…48.3
  • Display frame — rim walls to the PCB plane along both long sides (starting 1.5 mm in from the tail edge, clear of LED2) and two stubs beside the FFC passage on the ABXY side; open on the D-pad side where the tail U-folds
  • D-pad 24 × 6.5, A/B/X/Y Ø9 (offsets follow the board's DFM shift: Y at −9), Start/Select 10 × 5, Menu 12 × 3.8 — every cutout has a 3 mm guide well with a countersunk flange seat
  • LED light pipes Ø2 at (−55, −30), (−48, −30); Ø1.2 at (54/60/66/72, −20.5)
  • 4 screw bosses Ø7 with M2.5 insert sockets at the PCB corner holes

Back Panel (bottom shell)​

  • 3 mm fillet all round the back face, 8.9 mm plan corners (V3.1, concentric with the PCB's)
  • L/R lever faces 14 × 8 at (±58.8, 32) in 14.8 × 8.8 cutouts; "L"/"R" engraved beside them at (±47.8, 32)
  • Speaker grille — Ø22 hole pattern at (−62.5, 8), seat ring Ø32 for the 28 mm driver (player's left, the D-pad side)
  • 4 × M2.5 counterbores Ø5 at (±70, ±30.5)

Bottom Edge​

  • USB-C 9.0 × 3.2 hole — the plug's own size — at Z 12.8…16.0 on the J1 axis, straight through the 2.6 mm wall, no recess (V3.1); J1's mouth 3.1 mm inside the face
  • SD card slit 16 × 3.5 centred at Z = 14.2, x = 60, bottom shell only (the V2.1 slit; the card end is 2.1 mm inside the face); no guide shelf any more — the wall is at the PCB edge
  • Power slider window 12 mm wide at x = −40, sill Z 12, open to the split; the reinforced shuttle carries the knob (3.0 mm inside the face) out to a nub 1.2 proud

Internal (bottom shell)​

  • Battery pocket 70 × 37 (the user's 103665 cell 65 × 36 × 10, +5 lead clearance) with an 8 mm border, centred at (0, 0), lead notch on +X at y = −10
  • L/R lever hinges at x = ±50.4: two 3 × 2 blocks per side at y 25.5…27.5 and 36.5…38.5, a Ø2 rod at Z 4 printed with the shell
  • USB-C cradle 9.6 × 6.5 on the floor under J1, top 0.3 mm under its body
  • Battery hold-down — 4 posts 8 × 3 × 10 on the outer face of the ±Y border walls with Ø2.7 peg holes; 2 printed straps 5 × 1 × 59 mm with Ø2.4 pegs (3d_case/battery_strap_x2.stl, print 2)
  • Column gussets — 4 per bottom column, 1.2 × 6 × 8 mm
  • PCB edge ribs — 6 blocks under the PCB edge (±40 top, ±25 bottom, ±80 sides) so the board cannot flex under the D-pad
  • Speaker seat

Physical Layout​

Enclosure layout to scale: front with display, D-pad, ABXY, Start/Select/Menu, LEDs and bottom-edge ports; back with mirrored L/R levers, speaker grille and screw counterbores

Left/right convention

The FRONT view is what the player sees; the BACK view is what you see when you physically turn the device around, so left/right are mirrored. USB-C, the SD slot and the power switch are on the bottom side of the PCB; in device terms SD is on the ABXY side, the power switch and speaker on the D-pad side.

Dimensions Reference​

ElementDimensionNotes
Overall body165.8 × 80.8 × 26.6 mmPCB + 2 × (0.3 gap + 2.6 wall); 26.6: the 7.0 mm display stack
Wall thicknessfloor/front 2.0, side walls 2.6side = 1.2 skin + 1.2 lip + 0.2 clearance
Corner radius8.9 mm plan (PCB r 6 + 2.9); back fillet 3, front fillet 2V3.1
Panel outline pocket94.57 × 60.88 + 0.3/sideglass centred at x = 0.75 between the Select and Y caps; tail border 8.5 on the D-pad side (user: 8-9 mm)
Display viewport83.52 × 55.68 + 0.3/sideactive area at (3.7, 2); r 1.0 corners
Cable riser under the panel3.1 mmdisp_riser
D-pad cutout24 × 6.5 crossstems at r 9 = board DPAD_OFFSETS
Face button holesØ9A/B/X/Y at board ABXY_OFFSETS; Y flange clipped on the glass side
Start/Select10 × 5 pillsSelect flange clipped on the glass side
Menu12 × 3.8 pill1.2 mm web to the LED4/LED5 light pipes
L/R leverface 14 × 8, 1 mm proud of the backhinge rod x = ±50.4; nub Ø3.4 to Z 14.3 on SW11/SW12 at (±65, 32)
USB-C opening9.0 × 3.2, no recessthe plug shell 8.25 × 2.4 + clearance, Z 12.8–16.0
SD slit16 × 3.5 (Z 12.5…16.0, open to the split)bottom shell only; card end 2.1 inside the face
Power slidercore 10, window 12 (sill Z 12), flange 16 (1.25 / 3.75), notch 2, nub 8 × 3 × 1.2x = −40; knob travel 1.6
Battery pocket70 × 38 × 10.7103665 cell 65 × 36 × 10 + 5 mm leads; floor sunk 0.7 (1.3 left), border 1.5 × 8 tall; centre (38, 0)
Battery straps2 × (5 × 1.2 × 61.4)at x = −30 / +26, Z 12…13.2; posts 8 × 5.2 × 10
Bottom columnØ6 / Ø2.8, outer half-column for the last 2 mm, 4 gussets 1.513.4 mm tall
Top bossØ7.2, insert socket Ø3.1 × 4.5 + relief Ø2.8 × 2.07 mm tall, plain cylinder, 2 mm wall
Speakergrille Ø22 at (−62.5, 8), seat ring Ø32 / Ø28.628 mm driver, 5 mm thick

Printing​

PartSelectorPrint fileOrientation
Top shellcase_top_print3d_case/case_top.stlfront face down (bosses, frame and wells grow upward — no supports)
Bottom shellcase_bottom3d_case/case_bottom.stlback face down; the 3 mm back fillet prints as a shallow overhang
D-pad, A/B/X/Y, Start/Select/Menupart_*_print3d_case/*.stlface down (turned over about X); the conical flange grows upward, no supports; on FDM use a brim on Start/Select/Menu (small flat on the bed)
L/R levers (L and R, mirrored)part_shoulder_l, part_shoulder_r3d_case/lever_l.stl, 3d_case/lever_r.stlface down, as in the assembly
Power sliderpart_power_slider_print3d_case/power_slider.stllower block's inner face down, nub up
Plate: every part but the two shells—3d_case/merged.stlone bed 114 × 99 mm, built from the files above
Battery straps (×2)part_strap_print3d_case/battery_strap_x2.stlflat, pegs up
ParameterValue
MaterialPLA or PETG (caps in PETG or TPU feel better)
Layer height0.2 mm shells, 0.12 mm caps
Infill20 % shells, 100 % caps and slider
Supportsnone
Tolerances built in0.3 mm per side on caps and the panel pocket, 0.3 mm on the alignment lip

Assembly order​

The top shell goes first, upside down; the PCB finds its position in it against the six fences, with its corner holes on the inserts.

  1. Press the 4 M2.5 inserts (D3.5 × L4) into the top bosses (from the PCB side, soldering iron ~200 °C, flush with the boss face).
  2. Tape the panel to the bezel from inside the top shell (glass against the ceiling), tail on the D-pad side.
  3. Drop the face caps into their tubes from inside, face first, until the shoulder meets the ceiling (the D-pad: flange cone into its well's countersink). Seat the D-pad centring cup on the D-pad's pivot ball, dimple down, with a piece of double-sided tape on its flat top: when the PCB goes in (step 3) the cup sticks to it at exactly the pivot's position, no measuring. If the D-pad still walks off axis on the print, the clean fix is an NPTH under the pivot at the next PCB respin, and the cup goes away.
  4. Push each L/R lever's face out through its cutout in the back and hook its tongue over the hinge rod (flange inside the floor); drop the power slider into its window from inside (nub out, notch up, centred); seat the speaker in its ring; cell in the pocket, leads through the +X notch to J3; drop the two straps' pegs into the post holes.
  5. PCB onto the columns and ribs; fold the tail under the glass into the extension board (left of the slot), FFC through the slot into J4.
  6. Close and drive the 4 × M2.5 screws from the back.

Customization​

All dimensions are parameterized in enclosure.scad; every column-0 constant is part of the sync-gate contract (numbers, names and + - * / only).

disp_border_tail = 8.5;  // measure the panel's driver-ledge border and set it
disp_riser = 3.1; // cable space under the glass
bat_w = 90; bat_h = 50; bat_d = 10; // cell L x W x T (measured) — another cell: change these + R10
bat_offset_x = 2.6; bat_offset_y = 5;

Run the three gates after any change:

GateWhat it provesChecks
verify_enclosure_syncthe scad agrees with board.py (positions, holes, panel spec, all 98 parts) and the datasheets (switch incl. the TS-1187A cover and actuator, JST, ESP32, cell, MSK12C02, TF-01A); straps/posts and the L/R levers and pivot blocks clear every bottom part; the levers' arm on SW11/SW12, 5.6 / 5.6 arms, swing under the cover, pin held in its slot; slider, SD slit and USB-C recess vs the board33 + 21 mutations
verify_enclosure_requirementsthe model still does what was asked: 7.0 mm stack, glass between the caps, tail/extension zones, 6 LEDs, speaker side, insert/screw geometry, button sizes, 24 named minimum thicknesses, the measured 90 × 50 × 10 cell, the hold-down straps, engraved labels beside their own button and not mirrored, a measured thin-wall audit (OpenSCAD slices eroded by 0.6 mm), and the V3 asks: edge recesses, proud rounded caps, rounded shell19 + 23 mutations
verify_enclosure_collisionthe shells share no volume with the PCB parts, panel stack, battery, speaker or caps; caps vs panel; panel vs PCB parts (OpenSCAD CGAL)—

| verify_enclosure_stl | the other road: no scad constants — it slices 3d_case/case_top.stl, case_bottom.stl, the L/R caps and levers, the slider and the viewer parts with OpenSCAD import() and measures envelopes, every front cutout vs its switch/LED placement, the viewport vs the panel datasheet, the glass pocket and its gaps, insert sockets (Ø3.1 × 4.5, 2 mm wall), counterbores/bores/half-columns, pocket vs the measured cell and J3, the edge openings (L/R windows, slider slot, USB-C recess depth, SD slit width), grille side, cap/edge-part/strap/display alignment and thin walls on the STL slices; refuses STLs older than the scad | 11 |

All four run in make verify-all; make dispatch routes a red one to the cad-engineer agent. make export-enclosure-stl ends with the collision gate and the STL audit.

Modular Design​

FileContents
enclosure.scadConstants contract, filleted top/bottom shells, face caps, L/R caps and levers, power slider, lever pivot blocks, views, collision_check, labels_check
pcb_parts.scadGenerated by scripts/generate_enclosure_pcb.py from board.py — outline, slot, holes, 98 parts with body L × W × H
modules/buttons.scad2D cutout shapes, guide wells with countersink, conical cap flanges
modules/display.scadViewport cutout and bezel
modules/ports.scadUSB-C, SD card, power switch, speaker grille cutouts
modules/battery.scadBattery compartment primitives