"""
The Music Box Crank: a stepper motor turns the crank of a paper-strip music
box. The motor stands on an L-bracket at one end of a bar, a flexible coupler
joins its shaft to the music box's crank shaft, and the music box sits on a
wooden riser held by a corner bracket, so the two shafts line up. The punched
strip runs through the music box across the bar. The driver sits on the bar's
other end, and the ESP32-S3 board on the bench just past it.

Run from the cad/ folder:

    uv run music-box-crank/model.py

It writes, for the site:
    site/public/files/music-box-crank/music-box-crank.step    the whole assembly, for editing
    site/public/files/music-box-crank/music-box-crank.glb     the 3D model shown on the page
    site/public/files/music-box-crank/music-box-crank-cad.py  this script, and console.py beside it
    site/src/content/instruments/music-box-crank/drawings/music-box-crank-render.svg

Every part is bought, so there is nothing to print. The parts are drawn only
well enough to show where they go. Coordinates in millimeters: X along the
bar, Y across it, Z up from the bench.
"""

from pathlib import Path
import math
import sys

from build123d import Box, Plane, Polygon, extrude

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import console as c  # noqa: E402

BAR_L = 250.0
AXIS = (10.0, 50.0)  # the line of both shafts, across the bar and up: 30 mm above the bar
MOTOR_X = 87.0  # the motor's face, against its bracket
COUPLER = (98.0, 123.0)  # where the coupler runs along the bar
BOX = (140.0, 200.0, 45.0, 25.0)  # the music box: x from, x to, across, tall
RISER = 18.0  # the wood under it, so its crank shaft is level with the motor's
STRIP = (155.5, 41.0, -35.0, 265.0)  # the paper strip: its near edge along the bar, its width, and where it
# starts and ends across the bar, most of it already through the box and out the back

CAST = (0.5, 0.52, 0.55)
BLACK = (0.12, 0.12, 0.13)
VIEW = (0.62, -0.82, 0.48)  # from the front right, a little above


def corner(x, y, z, turn):
    """A cast corner bracket for 20 mm extrusion, its inside corner at (x, y, z): one leg
    flat, pointing `turn` degrees round from +X, the other standing up, a web down each side."""
    t, w = 4.0, 18.0
    shape = c.box(0, 20, -w / 2, w / 2, 0, t) + c.box(0, t, -w / 2, w / 2, 0, 20)
    for y0 in (-w / 2, w / 2 - 2):
        shape += c.Pos(0, y0 + 2, 0) * extrude(Plane.XZ * Polygon((t, t), (19, t), (t, 19), align=None), amount=2)
    return c.Pos(x, y, z) * c.Rot(0, 0, turn) * shape


def test_button(x):
    """The test button on a tiny plate screwed to the bar's front face, facing you."""
    return c.box(x - 10, x + 10, -3, 0, 6, 47) + c.Pos(x, -3, 36) * c.Rot(90, 0, 0) * c.push_button()


def motor():
    """A NEMA 17 stepper, 42.3 mm square with its corners cut, 40 long, its shaft along +X."""
    y, z = AXIS
    h = 42.3 / 2
    cut = c.Pos(MOTOR_X - 20, y, z) * c.Rot(45, 0, 0) * Box(40, (42.3 - 5) * math.sqrt(2), (42.3 - 5) * math.sqrt(2))
    body = c.box(MOTOR_X - 8, MOTOR_X, y - h, y + h, z - h, z + h) + c.box(MOTOR_X - 40, MOTOR_X - 32, y - h, y + h, z - h, z + h)
    body += c.box(MOTOR_X - 32, MOTOR_X - 8, y - h + 0.6, y + h - 0.6, z - h + 0.6, z + h - 0.6)  # the black middle, between its end caps
    return (body & cut) + c.cyl_x(y, z, 11, MOTOR_X, MOTOR_X + 2) + c.cyl_x(y, z, 2.5, MOTOR_X, MOTOR_X + 24)


def motor_bracket():
    """A bent steel L-bracket: its foot on the bar's top, its face across the bar."""
    y, z = AXIS
    foot = c.box(MOTOR_X - 37, MOTOR_X + 3, y - 23, y + 23, 20, 23)
    for x0 in (MOTOR_X - 32, MOTOR_X - 16):
        foot -= c.box(x0, x0 + 12, y - 2.75, y + 2.75, 19, 24)  # slots for the M5 screws into the bar
    face = c.box(MOTOR_X, MOTOR_X + 3, y - 23, y + 23, 20, 74) - c.cyl_x(y, z, 11.5, MOTOR_X - 1, MOTOR_X + 4)
    return foot + face


def coupler():
    """A flexible shaft coupler, its spiral cut drawn as rings."""
    y, z = AXIS
    x0, x1 = COUPLER
    shape = c.cyl_x(y, z, 9.5, x0, x1)
    for x in (x0 + 7, x0 + 10.5, x0 + 14, x0 + 17.5):
        shape -= c.cyl_x(y, z, 10, x, x + 0.8) - c.cyl_x(y, z, 8, x, x + 0.8)
    return shape


def music_box():
    """The movement, its strip slot across it and its crank shaft out of its left end."""
    x0, x1, d, h = BOX
    y, z = AXIS
    shape = c.box(x0, x1, y - d / 2, y + d / 2, 20 + RISER, 20 + RISER + h)
    shape -= c.box(STRIP[0] - 1, STRIP[0] + STRIP[1] + 1, y - d, y + d, z, z + 2.5)  # where the strip runs through
    return shape + c.cyl_x(y, z, 2, COUPLER[1] - 10, x0)  # the crank shaft, its handle off


def strip():
    """The punched paper strip, flat, with a scatter of notes punched in it."""
    x0, w, y0, y1 = STRIP
    y, z = AXIS
    shape = c.box(x0, x0 + w, y0, y1, z + 1.1, z + 1.4)
    for k, ny in enumerate(range(int(y0) + 5, int(y1), 8)):
        if abs(ny - y) > BOX[2] / 2 + 4:
            shape -= c.cyl_z(x0 + 3 + (k * 7) % 15 * 2.5, ny, 1.1, z, z + 2)
    return shape


def driver(x0, x1):
    """A TMC2209 driver on its carrier board, with a heat sink and a terminal for the motor."""
    shape = c.box(x0, x1, -5, 25, 20, 21.6) + c.box(x0 + 1, x0 + 9, -3, 5, 21.6, 30)
    shape += c.box(x0 + 9, x1 - 2, 2, 22, 21.6, 29) + c.box(x0 + 8, x1 - 1, 1, 23, 29, 30.6)  # its headers and the module
    return shape + c.box(x0 + 12, x1 - 4, 7, 17, 30.6, 35)


def main():
    x0, x1, d, _ = BOX
    y, _ = AXIS
    bought = [
        ("bar", c.vslot(0, BAR_L, 0, 20, 0, 20), (0.75, 0.76, 0.78)),
        ("motor-bracket", motor_bracket(), (0.35, 0.36, 0.38)),
        ("motor", motor(), BLACK),
        ("coupler", coupler(), (0.7, 0.71, 0.73)),
        ("box-bracket", corner(x0, y, 20, 180), CAST),
        ("riser", c.box(x0, x1, y - d / 2, y + d / 2, 20, 20 + RISER), (0.72, 0.55, 0.36)),
        ("music-box", music_box(), (0.78, 0.62, 0.3)),
        ("strip", strip(), (0.95, 0.94, 0.9)),
        ("driver", driver(202, 227), (0.25, 0.2, 0.45)),
        ("esp32-s3-board", c.Pos(BAR_L + 41, 10, 0) * c.boards(), (0.1, 0.25, 0.15)),
        ("test-button", test_button(238), (0.15, 0.15, 0.15)),
    ]
    look = (140, 10, 40)
    eye = tuple(a + 1000 * k for a, k in zip(look, VIEW))
    caption = "THE STEPPER, THE COUPLER, AND THE MUSIC BOX ON ITS RISER WITH THE STRIP THROUGH IT"
    c.export("music-box-crank", __file__, [], bought, [(None, eye, look, caption)], "The Music Box Crank")
    out = c.REPO / "site/public/files/music-box-crank"  # nothing is printed, so no STLs to offer
    (out / "music-box-crank-stl.zip").unlink()
    (out / "stl").rmdir()


if __name__ == "__main__":
    main()
