"""
The Servo Mallet: a micro servo swings a springy mallet, a long cable tie
with a wooden bead on its end, onto the rim of a small singing bowl. The
servo hangs in its bracket near the top of a short upright, its shaft facing
you, and the bowl sits on its cushion on the bench just past the bar's end,
in line with the mallet's swing. The model shows the mallet at the bottom of its swing, the bead just
short of the rim.

Run from the cad/ folder:

    uv run servo-mallet/model.py

It writes, for the site:
    site/public/files/servo-mallet/servo-mallet.step    the whole assembly, for editing
    site/public/files/servo-mallet/servo-mallet.glb     the 3D model shown on the page
    site/public/files/servo-mallet/servo-mallet-cad.py  this script, and console.py beside it
    site/src/content/instruments/servo-mallet/drawings/servo-mallet-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 (toward you is -Y), Z up from the bench.
"""

from pathlib import Path
import math
import sys

from build123d import Axis, Plane, Polygon, Solid, extrude, revolve

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

BAR_L = 250.0
UP_X = 110.0  # the upright's left face
UP_H = 150.0
SHAFT = (155.65, 145.0)  # the servo's shaft, along the bar and up
PLATE_Y = -2.5  # the front of the bracket's face, where the servo's tabs sit
STEM = 120.0  # the cable tie, from the horn to the bead
STEM_Y = (-22.8, -18.0)  # the tie lies flat against the front of the horn
BOWL = (295.0, sum(STEM_Y) / 2, 14.0)  # its middle, in line with the stem, and its bottom, sitting in the cushion
RIM = 38.75  # the middle of the bowl's wall, from its axis
GAP = 3.0  # between the bead and the rim, where the servo stops

ALUMINUM = (0.75, 0.76, 0.78)
CAST = (0.5, 0.52, 0.55)
BLACK = (0.12, 0.12, 0.13)
WHITE = (0.92, 0.92, 0.9)
WOOD = (0.72, 0.55, 0.36)
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 servo_parts(sx, sz):
    """The MG90S lying on its side, its tabs on the bracket's face and its shaft toward you."""
    x1 = sx + 6.0  # the body's end nearest the shaft
    x0 = x1 - 22.8
    servo = c.box(x0, x1, PLATE_Y - 7, PLATE_Y + 15.5, sz - 6.1, sz + 6.1)
    servo += c.box(x0 - 4.85, x1 + 4.85, PLATE_Y - 2.5, PLATE_Y, sz - 6.1, sz + 6.1)  # the tabs
    servo += c.cyl_y(sx, sz, 5.9, PLATE_Y - 11, PLATE_Y - 7)  # the gear tower, with the shaft in it
    foot = c.box(UP_X + 20, UP_X + 22.5, 1, 19, sz - 17, sz + 17)  # screwed to the upright's side
    face = c.box(UP_X + 20, x1 + 8, PLATE_Y, 0, sz - 17, sz + 17) - c.box(x0 - 0.5, x1 + 0.5, PLATE_Y - 1, 1, sz - 6.6, sz + 6.6)
    return servo, foot + face


def mallet(sx, sz, bx, bz):
    """The horn, the stem and the bead, swung down so the bead's middle is at (bx, bz)."""
    reach = math.hypot(bx - sx, bz - sz)
    swing = c.Pos(sx, 0, sz) * c.Rot(0, math.degrees(math.atan2(sz - bz, bx - sx)), 0)
    horn = c.cyl_y(0, 0, 3.5, STEM_Y[1] + 2, PLATE_Y - 11) + c.box(0, 17, STEM_Y[1], STEM_Y[1] + 2, -2.5, 2.5)
    horn += c.cyl_y(17, 0, 2.5, STEM_Y[1], STEM_Y[1] + 2)
    stem = c.box(reach - 3 - STEM, reach - 3, *STEM_Y, -0.65, 0.65)  # its end 3 mm into the bead
    return swing * horn, swing * stem, c.Pos(bx, sum(STEM_Y) / 2, bz) * Solid.make_sphere(6)


def revolved(points, x, y, z):
    """A round shape from its outline: distance out from the axis, and height."""
    return c.Pos(x, y, z) * revolve(Plane.XZ * Polygon(*points, align=None), Axis.Z)


def main():
    sx, sz = SHAFT
    bx0, by0, bz0 = BOWL
    my = sum(STEM_Y) / 2  # the plane the mallet swings in
    bead_x = bx0 - math.sqrt(RIM**2 - (my - by0) ** 2)  # where that plane crosses the rim, on the servo's side
    servo, bracket = servo_parts(sx, sz)
    horn, stem, bead = mallet(sx, sz, bead_x, bz0 + 40 + GAP + 6)
    bowl = [(0, 0), (26, 0), (34, 5), (38.5, 14), (40, 26), (40, 40), (37.5, 40), (37.5, 27), (36, 16), (31.5, 8.5), (24, 2.5), (0, 2.5)]
    cushion = [(0, 0), (45, 0), (45, 8), (43.5, 12.5), (40, 15), (0, 15)]
    bought = [
        ("bar", c.vslot(0, BAR_L, 0, 20, 0, 20), ALUMINUM),
        ("upright", c.vslot(UP_X, UP_X + 20, 0, 20, 20, 20 + UP_H), ALUMINUM),
        ("corner-bracket", corner(UP_X + 20, 10, 20, 0), CAST),
        ("servo-bracket", bracket, (0.7, 0.72, 0.75)),
        ("servo", servo, BLACK),
        ("horn", horn, WHITE),
        ("stem", stem, (0.88, 0.87, 0.8)),
        ("bead", bead, WOOD),
        ("bowl", revolved(bowl, *BOWL), (0.78, 0.62, 0.3)),
        ("cushion", revolved(cushion, bx0, by0, 0), (0.55, 0.12, 0.15)),
        ("esp32-s3-board", c.Pos(36, 10, 20) * c.boards(), (0.1, 0.25, 0.15)),
        ("test-button", test_button(82), (0.15, 0.15, 0.15)),
    ]
    look = (170, -10, 80)
    eye = tuple(a + 1000 * d for a, d in zip(look, VIEW))
    caption = "THE SERVO ON ITS UPRIGHT, THE MALLET AT THE BOTTOM OF ITS SWING, AND THE BOWL ON ITS CUSHION"
    c.export("servo-mallet", __file__, [], bought, [(None, eye, look, caption)], "The Servo Mallet")
    out = c.REPO / "site/public/files/servo-mallet"  # nothing is printed, so no STLs to offer
    (out / "servo-mallet-stl.zip").unlink()
    (out / "stl").rmdir()


if __name__ == "__main__":
    main()
