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