gokul/macro-six-usbc
USB-powered ATmega32U4 controller board with protected/switched 5 V power, six tactile keys, rotary encoder, reset/boot/ISP interfaces, power LED, and six daisy-chained SK6812 RGB LEDs driven through MOSFETs.
- Version
- 0.1.6
- License
- MIT
- Stars
- 0
enclosure/appearance.py
"""Styled keycap and knob fit references; dimensions are not a supplier specification."""
import math
import cadquery as cq
def rounded_wire(width, radius, z):
wire = cq.Workplane("XY").workplane(offset=z).rect(width, width).val()
return wire.fillet2D(radius, wire.Vertices())
def loft(sections):
return cq.Workplane("XY").add(cq.Solid.makeLoft(
[rounded_wire(w, r, z) for w, r, z in sections], ruled=True))
def cylinder(radius, height, z):
return cq.Workplane("XY").circle(radius).extrude(height).translate((0, 0, z))
def make_appearance(keys, pcb_top):
parts = []
bottom = pcb_top + 10.5
# Rounded shoulders, an open skirt, and an MX cross socket replace solid blocks.
shell = loft([(18.5, 1.0, bottom), (18.1, 1.2, bottom + 1.1),
(14.8, 2.0, bottom + 9.6), (14.4, 2.1, bottom + 10.6)])
dish = cq.Workplane("XY").sphere(42).translate((0, 0, bottom + 51.85))
shell = shell.cut(dish)
cavity = loft([(16.1, .65, bottom - .1), (11.8, 1.0, bottom + 8.5)])
shell = shell.cut(cavity)
socket_bottom = bottom + 1.8
socket = cylinder(2.85, 7.3, socket_bottom)
# Supplier mesh cross: 4.0 x 4.0, with 1.3 and 1.1 mm arms.
cross = (cq.Workplane("XY").rect(4.16, 1.46).extrude(3.05)
.union(cq.Workplane("XY").rect(1.26, 4.16).extrude(3.05))
.translate((0, 0, socket_bottom - .1)))
shell = shell.union(socket).cut(cross)
assert shell.val().isValid() and len(shell.val().Solids()) == 1
for i, (x, y) in enumerate(keys, 1):
# Legends are clipped to the dished surface and inset, with a contrasting fill.
text = (cq.Workplane("XY").text(str(i), 3.3, 4, font="Arial", kind="bold",
halign="center", valign="center", combine=True)
.translate((0, 2.0, bottom + 8.8)))
legend = text.intersect(shell)
body = shell.cut(text)
color = [61, 132, 150, 255] if i == 6 else [229, 226, 216, 255]
parts.extend([
dict(name=f"keycap-reference-{i}", asset="keycaps-reference",
solid=body.translate((x, y, 0)), color=color),
dict(name=f"keycap-reference-{i}-legend", asset="keycaps-reference",
solid=legend.translate((x, y, 0)),
color=[242, 243, 238, 255] if i == 6 else [54, 66, 73, 255]),
])
# Center on the actual supplier shaft, whose axis is 0.25 mm above its CAD origin.
knob_center = (43.0, .2499873, 0)
knob = cylinder(9.0, 15.0, 18.8).edges().fillet(.65)
for angle in range(0, 360, 10):
theta = math.radians(angle)
flute = cylinder(.30, 12.9, 19.6).translate((9.06 * math.cos(theta),
9.06 * math.sin(theta), 0))
knob = knob.cut(flute)
# Recess clears the encoder bushing; the upper bore clears the 6 mm shaft.
knob = knob.cut(cylinder(4.15, 4.0, 18.7)).cut(cylinder(3.2, 14.6, 18.7))
knob = knob.cut(cylinder(7.55, 1.0, 33.35))
inlay = cylinder(7.45, .75, 33.35).edges(">Z").fillet(.20)
marker = (cq.Workplane("XY").box(.85, 2.6, .18, centered=(True, True, False))
.edges("|Z").fillet(.30).translate((0, 5.3, 34.00)))
inlay = inlay.cut(marker)
parts.extend([
dict(name="knob-reference-body", asset="knob-reference", solid=knob.translate(knob_center),
color=[45, 53, 61, 255]),
dict(name="knob-reference-inlay", asset="knob-reference", solid=inlay.translate(knob_center),
color=[185, 197, 202, 255], metallic=.75, roughness=.35),
dict(name="knob-reference-marker", asset="knob-reference", solid=marker.translate(knob_center),
color=[64, 182, 198, 255]),
])
for part in parts:
assert part["solid"].val().isValid(), part["name"]
return parts