"""
Coral Eye twist shutter: turn the knurled ring by 60 degrees and two blades close over the lens.

Stack, from the face outwards. Local frame: origin on the lens axis at the face's front
surface, z outwards. The module is screwed to the face with two M2 x 6 button-head screws.

  * twist ring (knurled grip and thumb lug) lies on the face, centred by a short sleeve that
    turns inside the face's eye, and turns by SWEEP degrees. Two spiral cam grooves on its
    front drive the blades' pins; two arc slots let the bezel's bosses through and are the end
    stops. Beside each slot a curved beam carries two wedge nubs: the boss snaps past them just
    before each end of the travel (the click), in the ring's plane, where FDM is accurate.
  * two blades slide in a keyed channel on the bezel's back, one up and one down. Their tips
    meet with a 45-degree scarf joint, so the closed shutter is light-tight.
  * bezel (fixed, the visible front): carries the channel, the two bosses that stand on the
    face and the screw counterbores. The bosses set 0.2 mm of play for the 2.4 mm ring, so it
    turns freely however tight the screws are.

Every part prints flat without supports: bezel front-down, twist ring front-down (sleeve up),
blades front-down (pins up).
"""
import math

from build123d import *

import coral_eye as ce

R_EYE = ce.EYE_D / 2                 # 5.8, same as the face's eye
SWEEP = 60.0                         # degrees from open to closed
TRAVEL = 7.0                         # each blade
CL = 0.2                             # sliding clearance per side

# twist ring (heights are whole 0.2 mm layers)
TR_R = 19.0
TR_Z0, TR_Z1 = 0.0, 2.4
KNURL_N, KNURL_D = 40, 1.0
LUG_ANGLE, LUG_R, LUG_W = 150.0, 22.0, 6.0     # thumb lug: sweeps the left side, 150 -> 210 degrees

# blades (closed position; the "upper" blade covers y >= 0 and moves +y to open)
BL_Z0, BL_T = 2.6, 1.0
HEAD_W, HEAD_END, HEAD_R = 12.6, 6.6, 2.5
STEM_W = {True: 4.0, False: 3.2}       # upper / lower: keyed, each blade only fits its own half
PIN_D, PIN_L = 2.0, 1.4
PIN_R_CLOSED = 7.9
STEM_END = 9.2
SCARF = 0.5                          # the 45-degree scarf spans y -0.5 .. +0.5 at the tips
SCARF_GAP = 0.1

# cam grooves (front side of the twist ring)
GROOVE_W = PIN_D + 0.4
GROOVE_D = 1.6
GROOVE_EXTRA = 9.0                   # degrees of dwell (constant radius) past each end of the travel

# bezel
BZ_R = 17.5
BZ_Z0 = 2.6                          # back plane = boss height: 0.2 mm of play for the 2.4 mm ring
CH_D = 1.2                           # channel depth: 0.2 mm of play for the 1.0 mm blades
BZ_Z1 = BZ_Z0 + CH_D + 1.2           # front, 5.0
BORE_R = 6.8                         # straight bore above the channel, then a 45-degree hood
BOSS_D, BOSS_X = 4.4, ce.LENS_SCREW_DX
SCREW_HOLE_D, CBORE_D = 2.5, 4.0     # M2 x 6 button head (ISO 7380) seated on the boss
SLOT_CL = 0.3

# detent, in the ring's plane (where FDM is accurate): beside each arc slot a 0.7 mm curved
# beam, anchored in the middle and free at both ends, carries a wedge nub (40-degree flanks)
# near each end; the boss snaps past it just before each end of the travel. 0.3 mm
# interference: ~2 N at the grip in PLA (~30 MPa in the beam), ~1.3 N in PETG (~18 MPa)
BEAM_T, RELIEF_W = 0.7, 0.8
NUB_H, NUB_FLANK = 0.6, 40.0         # nub height into the slot, flank angle to the slot wall
NUB_OFF = 6.2                        # degrees from each end position (the boss rests 0.19 mm clear)
FREE_END = 4.0                       # degrees past each end position where the beam is cut free
ANCHOR = 4.0                         # degrees of beam left attached in the middle

# centring sleeve under the twist ring: turns inside the face's eye (R_EYE)
SLEEVE_RO, SLEEVE_RI, SLEEVE_L = R_EYE - 0.15, 4.85, 1.0      # 0.8 mm wall

FACE_PILOT_D, FACE_PILOT_DEPTH = ce.LENS_SCREW_PILOT


def _cyl(x, y, z0, z1, d):
    return Pos(x, y, z0) * Cylinder(radius=d / 2, height=z1 - z0, align=(Align.CENTER, Align.CENTER, Align.MIN))


def _polar(r, deg):
    a = math.radians(deg)
    return r * math.cos(a), r * math.sin(a)


def _band(pts, width, z0, height):
    """A curved path of the given width (flat ends), extruded up from z0. Built as one polygon
    from both offset edges, so a curved path leaves no slivers at the segment joints."""
    left, right = [], []
    n = len(pts)
    for i, (x, y) in enumerate(pts):
        xa, ya = pts[max(i - 1, 0)]
        xb, yb = pts[min(i + 1, n - 1)]
        tx, ty = xb - xa, yb - ya
        tl = math.hypot(tx, ty)
        nx, ny = -ty / tl * width / 2, tx / tl * width / 2
        left.append((x + nx, y + ny))
        right.append((x - nx, y - ny))
    return Pos(0, 0, z0) * extrude(Polygon(*(left + right[::-1]), align=None), amount=height, dir=(0, 0, 1))


def groove_radius(theta):
    """Pin radius when the twist ring is turned by theta (0 = open, SWEEP = closed)."""
    return PIN_R_CLOSED + TRAVEL * (1.0 - theta / SWEEP)


def blade_offset(theta):
    return TRAVEL * (1.0 - theta / SWEEP)


# --------------------------------------------------------------------------
# blades
# --------------------------------------------------------------------------
def _blade_outline(upper=True):
    """Head + stem of a blade in the closed position, drawn on the +y side."""
    y0 = -SCARF - 0.3
    w = STEM_W[upper]
    head = Pos(0, (y0 + HEAD_END) / 2) * Rectangle(HEAD_W, HEAD_END - y0)
    head = fillet(head.vertices().group_by(Axis.Y)[-1], radius=HEAD_R)
    c = STEM_END - w / 2
    stem = Pos(0, (HEAD_END - 1.0 + c) / 2) * Rectangle(w, c - (HEAD_END - 1.0)) + Pos(0, c) * Circle(w / 2)
    return head + stem


def make_blade(upper=True):
    """Blade in the closed position, local frame."""
    sk = _blade_outline(upper)
    if not upper:
        sk = mirror(sk, about=Plane.XZ)
    b = Pos(0, 0, BL_Z0) * extrude(sk, amount=BL_T)
    # 45-degree scarf: the upper blade keeps y >= z' - SCARF, the lower one y <= z' - SCARF
    # (z' measured from the blades' back), with SCARF_GAP between them
    off = SCARF_GAP / 2 * math.sqrt(2)
    z0, z1, big = BL_Z0 - 0.01, BL_Z0 + BL_T + 0.01, 40.0
    if upper:
        pts = [(-SCARF + off - 0.01, z0), (big, z0), (big, z1), (SCARF + off + 0.01, z1)]
    else:
        pts = [(-big, z0), (-SCARF - off - 0.01, z0), (SCARF - off + 0.01, z1), (-big, z1)]
    b = b & extrude(Plane.YZ.offset(-20) * Polygon(*pts, align=None), amount=40)
    s = 1 if upper else -1
    b += _cyl(0, s * PIN_R_CLOSED, BL_Z0 - PIN_L, BL_Z0 + 0.01, PIN_D)
    return b


# --------------------------------------------------------------------------
# twist ring
# --------------------------------------------------------------------------
def make_twist_ring():
    ring = _cyl(0, 0, TR_Z0, TR_Z1, 2 * TR_R) - _cyl(0, 0, TR_Z0 - 1, TR_Z1 + 1, 2 * R_EYE)
    top_outer = ring.edges().filter_by(GeomType.CIRCLE).group_by(Axis.Z)[-1].sort_by(SortBy.RADIUS)[-1]
    ring = chamfer(top_outer, length=0.4)
    for i in range(KNURL_N):                                     # knurled grip
        x, y = _polar(TR_R + 0.15, i * 360.0 / KNURL_N)
        ring -= _cyl(x, y, TR_Z0 - 1, TR_Z1 + 1, KNURL_D)
    # spiral cam grooves: in the ring's frame, blade pins sit at (90 - theta) and (270 - theta);
    # past both ends of the travel the groove keeps a constant radius (dwell), clear of the bore
    # (cut as three bands, spiral and two arcs, plus a round at each kink: one polygon with a
    # kink would self-intersect on its inner side)
    for base in (90.0, 270.0):
        for t0, t1 in ((-GROOVE_EXTRA, 0.0), (0.0, SWEEP), (SWEEP, SWEEP + GROOVE_EXTRA)):
            pts = [_polar(groove_radius(min(max(t0 + (t1 - t0) * k / 30, 0.0), SWEEP)), base - (t0 + (t1 - t0) * k / 30))
                   for k in range(31)]
            ring -= _band(pts, GROOVE_W, TR_Z1 - GROOVE_D, GROOVE_D + 1.0)
        for th in (0.0, SWEEP):
            x, y = _polar(groove_radius(th), base - th)
            ring -= _cyl(x, y, TR_Z1 - GROOVE_D, TR_Z1 + 1.0, GROOVE_W)
    # arc slots around the bezel's bosses: they are also the end stops
    half = math.degrees((BOSS_D / 2 + SLOT_CL) / BOSS_X)
    for base in (0.0, 180.0):
        a0, a1 = base - SWEEP - half, base + half
        pts = [_polar(BOSS_X, a0 + (a1 - a0) * k / 60) for k in range(61)]
        ring -= _band(pts, BOSS_D + 2 * SLOT_CL, TR_Z0 - 1.0, TR_Z1 - TR_Z0 + 2.0)
    # in-plane detent beams with wedge nubs (see the parameters above)
    slot_out = BOSS_X + BOSS_D / 2 + SLOT_CL
    rel_c = slot_out + BEAM_T + RELIEF_W / 2
    half_base = math.degrees(NUB_H / math.tan(math.radians(NUB_FLANK)) / slot_out)
    for base in (0.0, 180.0):
        open_end, closed_end, mid = base + FREE_END, base - SWEEP - FREE_END, base - SWEEP / 2
        for a0, a1 in ((closed_end, mid - ANCHOR / 2), (mid + ANCHOR / 2, open_end)):
            pts = [_polar(rel_c, a0 + (a1 - a0) * k / 30) for k in range(31)]
            ring -= _band(pts, RELIEF_W, TR_Z0 - 1.0, TR_Z1 - TR_Z0 + 2.0)
        for a in (mid - ANCHOR / 2, mid + ANCHOR / 2):          # round relief ends at the anchor
            x, y = _polar(rel_c, a)
            ring -= _cyl(x, y, TR_Z0 - 1.0, TR_Z1 + 1.0, RELIEF_W)
        for a in (open_end, closed_end):                        # cut each free end loose
            p0, p1 = _polar(slot_out - 0.3, a), _polar(rel_c + RELIEF_W / 2 + 0.05, a)
            ring -= _band([p0, p1], RELIEF_W, TR_Z0 - 1.0, TR_Z1 - TR_Z0 + 2.0)
        for a in (base - NUB_OFF, base - SWEEP + NUB_OFF):
            nub = Polygon(_polar(slot_out + 0.1, a - half_base - 0.4), _polar(slot_out - NUB_H, a),
                          _polar(slot_out + 0.1, a + half_base + 0.4), align=None)
            ring += Pos(0, 0, TR_Z0) * extrude(nub, amount=TR_Z1 - TR_Z0, dir=(0, 0, 1))
    # centring sleeve: turns inside the face's eye. The ring's bore narrows to it through a
    # 45-degree cone (an overhang that prints, front-down), still clear of the field of view
    z_cone = TR_Z0 + 0.6
    top = z_cone + (R_EYE - SLEEVE_RI)
    prof = [(SLEEVE_RI, -SLEEVE_L), (SLEEVE_RO, -SLEEVE_L), (SLEEVE_RO, TR_Z0), (R_EYE + 0.2, TR_Z0),
            (R_EYE + 0.2, top), (R_EYE, top), (SLEEVE_RI, z_cone)]
    ring += revolve(Plane.XZ * Polygon(*prof, align=None), axis=Axis.Z)
    # thumb lug
    lug = Pos(0, 0, TR_Z0) * extrude(Pos(TR_R - 1.0 + (LUG_R - TR_R + 1.0) / 2, 0) * RectangleRounded(
        LUG_R - TR_R + 1.0, LUG_W, 1.4), amount=TR_Z1 - TR_Z0)
    ring += Rot(0, 0, LUG_ANGLE) * lug
    return ring


# --------------------------------------------------------------------------
# bezel
# --------------------------------------------------------------------------
def make_bezel():
    prof = [(BORE_R, BZ_Z0 + CH_D), (BORE_R + 1.2, BZ_Z1), (BZ_R - 0.5, BZ_Z1), (BZ_R, BZ_Z1 - 0.5),
            (BZ_R, BZ_Z0), (BORE_R, BZ_Z0)]
    bz = revolve(Plane.XZ * Polygon(*prof, align=None), axis=Axis.Z)
    # blade channel on the back: head slot + keyed stem slots, long enough for both blades fully open
    open_head = HEAD_END + TRAVEL + CL
    slots = fillet(Rectangle(HEAD_W + 2 * CL, 2 * open_head).vertices(), radius=HEAD_R + CL)
    stem_end = STEM_END + TRAVEL + CL
    for upper, s in ((True, 1), (False, -1)):            # keyed stem slots: the wide one takes the upper blade
        sw = STEM_W[upper] + 2 * CL
        slots += Pos(0, s * stem_end / 2) * SlotCenterToCenter(stem_end - sw, sw, rotation=90)
    bz -= Pos(0, 0, BZ_Z0 - 0.01) * extrude(slots, amount=CH_D + 0.01)
    # bosses that stand on the face; the screw heads sit on them, in counterbores
    for sx in (-1, 1):
        bz += _cyl(sx * BOSS_X, 0, 0.0, BZ_Z0 + 0.01, BOSS_D)
        bz -= _cyl(sx * BOSS_X, 0, -1.0, BZ_Z1 + 1.0, SCREW_HOLE_D)
        bz -= _cyl(sx * BOSS_X, 0, BZ_Z0, BZ_Z1 + 1.0, CBORE_D)
    return bz


# --------------------------------------------------------------------------
# placement
# --------------------------------------------------------------------------
def to_device(shape):
    return Pos(ce.LENS_X, ce.LENS_Y, ce.Z_FRONT) * shape


def assembly(theta, parts=None):
    """Parts in the device frame with the twist ring turned by theta (0 open, SWEEP closed)."""
    p = parts or {"bezel": make_bezel(), "twist": make_twist_ring(),
                  "blade_up": make_blade(True), "blade_down": make_blade(False)}
    d = blade_offset(theta)
    return {
        "bezel": to_device(p["bezel"]),
        "twist": to_device(Rot(0, 0, theta) * p["twist"]),
        "blade_up": to_device(Pos(0, d, 0) * p["blade_up"]),
        "blade_down": to_device(Pos(0, -d, 0) * p["blade_down"]),
    }


def face_pilots():
    """Blind pilot holes for the two M2 screws, cut into the face from its front."""
    holes = [_cyl(ce.LENS_X + sx * BOSS_X, ce.LENS_Y, ce.Z_FRONT - FACE_PILOT_DEPTH, ce.Z_FRONT + 1, FACE_PILOT_D)
             for sx in (-1, 1)]
    return holes[0] + holes[1]


def screw_heads():
    """Render-only: the two M2 button heads (ISO 7380) seated on the bosses, with a hex socket."""
    out = None
    for sx in (-1, 1):
        head = _cyl(sx * BOSS_X, 0, BZ_Z0, BZ_Z0 + 0.75, 3.5)
        head += Pos(sx * BOSS_X, 0, BZ_Z0 + 0.75) * Sphere(radius=1.75) & _cyl(sx * BOSS_X, 0, BZ_Z0 + 0.74, BZ_Z0 + 1.1, 3.6)
        head -= Pos(sx * BOSS_X, 0, BZ_Z0 + 0.4) * extrude(RegularPolygon(radius=0.75, side_count=6), amount=1.0)
        out = head if out is None else out + head
    return out


# --------------------------------------------------------------------------
# export
# --------------------------------------------------------------------------
def print_parts():
    """Every shutter part in its print orientation, resting on z = 0, centred on the origin."""
    flip_bz = Pos(0, 0, BZ_Z1) * Rot(180, 0, 0)                 # bezel front-down
    flip_bl = Pos(0, 0, BL_Z0 + BL_T) * Rot(180, 0, 0)          # blades front-down, pins up
    out = {
        "CoralEye_Shutter_Bezel": flip_bz * make_bezel(),
        "CoralEye_Shutter_TwistRing": Pos(0, 0, TR_Z1) * Rot(180, 0, 0) * make_twist_ring(),   # front-down, sleeve up
        "CoralEye_Shutter_Blade_Upper": flip_bl * make_blade(True),
        "CoralEye_Shutter_Blade_Lower": flip_bl * make_blade(False),
    }
    for k, v in out.items():
        bb = v.bounding_box()
        out[k] = Pos(-(bb.min.X + bb.max.X) / 2, -(bb.min.Y + bb.max.Y) / 2, -bb.min.Z) * v
    return out


def export_all(render_folder=None):
    import os
    tol = dict(tolerance=0.005, angular_tolerance=0.1)
    for name, part in print_parts().items():
        export_stl(part, os.path.join(ce.OUT_DIR, name + ".stl"), **tol)
    a0 = assembly(0.0)
    for k, name in (("bezel", "Bezel"), ("twist", "TwistRing"), ("blade_up", "Blade_Upper"), ("blade_down", "Blade_Lower")):
        export_step(a0[k], os.path.join(ce.OUT_DIR, "CoralEye_Shutter_" + name + ".step"))
    if render_folder:
        parts = {"bezel": make_bezel(), "twist": make_twist_ring(),
                 "blade_up": make_blade(True), "blade_down": make_blade(False)}
        export_stl(to_device(screw_heads()), os.path.join(render_folder, "sh_screws.stl"), **tol)
        for th, tag in ((0.0, "open"), (SWEEP / 2, "half"), (SWEEP, "closed")):
            a = assembly(th, parts)
            for k, v in a.items():
                export_stl(v, os.path.join(render_folder, f"sh_{k}_{tag}.stl"), **tol)


if __name__ == "__main__":
    import os
    import sys
    rf = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "render", "meshes")) \
        if "--render-meshes" in sys.argv else None
    export_all(rf)
    if "--anim" in sys.argv:                 # meshes for the open/close animation
        import os as _os
        fd = _os.path.normpath(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..", "render", "meshes", "anim"))
        _os.makedirs(fd, exist_ok=True)
        parts = {"bezel": make_bezel(), "twist": make_twist_ring(),
                 "blade_up": make_blade(True), "blade_down": make_blade(False)}
        for i in range(13):
            a = assembly(SWEEP * i / 12, parts)
            for k, v in a.items():
                export_stl(v, _os.path.join(fd, f"f{i:02d}_{k}.stl"), tolerance=0.01, angular_tolerance=0.15)
    for name, part in print_parts().items():
        bb = part.bounding_box()
        print(f"{name:30s} {part.volume / 1000:5.2f} cm3  {bb.size.X:.1f} x {bb.size.Y:.1f} x {bb.size.Z:.2f}")
