"""
The Tapper: a 12 V frame solenoid that taps a block of wood on every note.
A short upright stands on a base bar, the solenoid hangs plunger down from a
corner bracket high on its side, and the block stands on the bar underneath.
The ESP32-S3 on its screw-terminal board, the MOSFET board and the test button
sit on the bar's left end.

Run from the cad/ folder:

    uv run tapper/model.py

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

Every part is bought, so there is nothing to print. The parts are drawn only
well enough to show where they go. The Stop Bar's script builds the same
Tapper with tapper() below. Coordinates in millimeters: X along the bar, Y
across it, Z up from the bench.
"""

from pathlib import Path
import sys

from build123d import Plane, Polygon, Solid, extrude

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

BAR_L = 300.0  # the base
UP_X = 160.0  # the upright's left face
UP_H = 120.0
BLOCK = (40.0, 60.0, 30.0)  # the wood: along the bar, across it, and tall
GAP = 6.0  # the plunger's free travel before it meets the block
SOL_X = 13.0  # the solenoid's axis, out from the upright's face

ALUMINUM = (0.75, 0.76, 0.78)
CAST = (0.5, 0.52, 0.55)
BLACK = (0.12, 0.12, 0.13)
WOOD = (0.72, 0.55, 0.36)
BOARD = (0.1, 0.25, 0.15)
VIEW = (0.62, -0.82, 0.48)  # from the front right, a little above


def prism_xz(points, y0, y1):
    """A prism of an outline drawn in X and Z, running from y0 to y1."""
    return c.Pos(0, y1, 0) * extrude(Plane.XZ * Polygon(*points, align=None), amount=y1 - y0)


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 += prism_xz([(t, t), (19, t), (t, 19)], y0, y0 + 2)
    return c.Pos(x, y, z) * c.Rot(0, 0, turn) * shape


def solenoid(x, y, top):
    """A JF-0530B-style open-frame push solenoid hanging from `top`: frame and coil 30 tall,
    13 along the bar and 16 across, its 7 mm plunger 10 mm out the bottom with its spring."""
    bottom = top - 30
    shape = c.box(x - 6.5, x + 6.5, y - 8, y + 8, top - 1.5, top) + c.box(x - 6.5, x + 6.5, y - 8, y + 8, bottom, bottom + 1.5)
    shape += c.box(x - 6.5, x + 6.5, y + 6.5, y + 8, bottom, top)  # the frame's back, away from you
    shape += c.cyl_z(x, y - 0.5, 6, bottom + 1.5, top - 1.5)  # the coil
    shape += c.cyl_z(x, y - 0.5, 3.5, bottom - 10, top - 4)  # the plunger
    for k in range(4):
        shape += c.Pos(x, y - 0.5, bottom - 1.2 - 1.6 * k) * Solid.make_torus(4.3, 0.6)
    return shape


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


def mosfet(x, z):
    """A small MOSFET board on short standoffs, with its input and output terminals."""
    shape = c.box(x, x + 25, -7, 27, z + 2, z + 3.6) + c.box(x + 9, x + 15, 7, 13, z + 3.6, z + 5)
    for y0 in (-6, 18):
        shape += c.box(x + 2.5, x + 22.5, y0, y0 + 8, z + 3.6, z + 12)
    for px in (x + 3, x + 22):
        for py in (-4, 24):
            shape += c.cyl_z(px, py, 2, z, z + 2)
    return shape


def tapper(x, z):
    """The upright with its two brackets, the solenoid and the block: x is the upright's
    left face and z the top of the bar it stands on."""
    bx = x + 21  # the block's left face, just clear of the upright
    top = z + BLOCK[2] + GAP + 10 + 30  # where the solenoid's frame meets its bracket
    block = c.box(bx, bx + BLOCK[0], 10 - BLOCK[1] / 2, 10 + BLOCK[1] / 2, z, z + BLOCK[2])
    block -= c.box(bx + 3, bx + BLOCK[0] - 3, -21, 22, z + BLOCK[2] - 13, z + BLOCK[2] - 10)  # its slot
    return [
        ("upright", c.vslot(x, x + 20, 0, 20, z, z + UP_H), ALUMINUM),
        ("corner-bracket", corner(x, 10, z, 180), CAST),
        ("solenoid-bracket", corner(x + 20, 10, top, 0), CAST),
        ("solenoid", solenoid(x + 20 + SOL_X, 10, top), (0.6, 0.6, 0.62)),
        ("block", block, WOOD),
    ]


def main():
    bought = [("base-bar", c.vslot(0, BAR_L, 0, 20, 0, 20), ALUMINUM)]
    bought += tapper(UP_X, 20)
    bought += [
        ("esp32-s3-board", c.Pos(45, 10, 20) * c.boards(), BOARD),
        ("mosfet-module", mosfet(112, 20), (0.1, 0.2, 0.45)),
        ("test-button", test_button(96), BLACK),
    ]
    look = (BAR_L / 2, 10, 60)
    eye = tuple(a + 1000 * d for a, d in zip(look, VIEW))
    c.export("tapper", __file__, [], bought, [(None, eye, look, "THE SOLENOID OVER ITS BLOCK, THE BOARD, THE MOSFET BOARD AND THE TEST BUTTON")], "The Tapper")
    out = c.REPO / "site/public/files/tapper"  # nothing is printed, so no STLs to offer
    (out / "tapper-stl.zip").unlink()
    (out / "stl").rmdir()


if __name__ == "__main__":
    main()
