"""Padel racket wall holder: parametric model -> padel_holder.stl (print-ready orientation).

Mounted coords: X = width, Y = out from the wall, Z = up.
The racket slides in face-against-wall; its head taper rests on the two slanted
side walls (a V cradle) and the front lips stop it tipping forward.
Run: python padel_holder.py
"""
import math
import numpy as np
import trimesh
from manifold3d import CrossSection, Manifold

# --- tune these (mm) ---
# Matches the listed size: 155 mm long x 30 mm tall x 50 mm deep.
T = 4.0            # wall thickness
GAP = 42.0         # space for the racket frame (rackets are <= 38 mm thick); depth = GAP + 2T = 50
W = 145.0          # inner width at the top, between the side walls (outer 155)
H = 30.0           # height of the bracket
ALPHA = 37.0       # side-wall slant from vertical (deg); bottom inner width ~100
STRIP = 30.0       # height of the back plate (= H: full back, like the original)
LIP = 22.0         # how far the front lips reach in over the racket face
TAB = 25.0         # extra back plate below the bracket for more tape area; 0 = original look
SCREWS = False     # True adds two countersunk screw holes; False = solid back for tape
HOLE = 4.4         # screw shank hole (for 4 mm wood screws)
HEAD = 8.4         # countersink diameter (90 deg head)
HOLE_SPACING = 70.0
BED = 256.0        # your printer bed (X/Y); 250 for Prusa MK4, 220 -> print diagonally

tan_a = math.tan(math.radians(ALPHA))
DEPTH = T + GAP + T


def x_in(z):  # left wall inner face at height z (mirror for the right)
    return -W / 2 + (H - z) * tan_a


def prism(pts_xz, y0, y1):
    """Extrude an XZ front-view polygon along Y from y0 to y1."""
    m = Manifold.extrude(CrossSection([pts_xz]), y1 - y0)   # XY profile, extruded along +Z
    m = m.rotate([90, 0, 0])                                 # profile Y -> Z, extrusion -> -Y
    return m.translate([0, y1, 0])


def side(sign):
    dx = T / math.cos(math.radians(ALPHA))                   # horizontal thickness of slanted wall
    xi = lambda z: sign * x_in(z)
    wall = [(xi(H) - sign * dx, H), (xi(H), H), (xi(0), 0), (xi(0) - sign * dx, 0)]
    lip = [(xi(H), H), (xi(H) + sign * LIP, H), (xi(0) + sign * LIP, 0), (xi(0), 0)]
    fix = lambda p: p if sign < 0 else p[::-1]               # keep CCW winding
    return prism(fix(wall), 0, DEPTH) + prism(fix(lip), DEPTH - T, DEPTH)


def build():
    z0 = H - STRIP
    o = T / 2  # sink the plate ends into the walls so the union is clean
    plate = [(x_in(z0) - o, z0), (-x_in(z0) + o, z0), (-x_in(H) + o, H), (x_in(H) - o, H)]
    body = prism(plate, 0, T) + side(-1) + side(1)
    if TAB:
        # sits behind the racket's throat; up to z = T so it overlaps the plate cleanly
        dx = T / math.cos(math.radians(ALPHA))
        a, b = x_in(0) - dx, -x_in(0) + dx
        body += prism([(a, -TAB), (b, -TAB), (b, T), (a, T)], 0, T)

    for sx in (-HOLE_SPACING / 2, HOLE_SPACING / 2) if SCREWS else ():
        shank = Manifold.cylinder(T + 2, HOLE / 2, HOLE / 2, 48).translate([0, 0, -1])
        sink_h = (HEAD - HOLE) / 2                           # 90 deg countersink depth
        sink = Manifold.cylinder(sink_h + 0.01, HOLE / 2, HEAD / 2, 48).translate([0, 0, T - sink_h])
        # cylinders run along +Z; turn them to run along +Y (head on the racket side, y = T)
        cut = (shank + sink).rotate([-90, 0, 0]).translate([sx, 0, H - STRIP / 2])
        body -= cut
    return body


def to_mesh(m):
    g = m.to_mesh()
    return trimesh.Trimesh(np.asarray(g.vert_properties)[:, :3], np.asarray(g.tri_verts), process=True)


def print_orientation(m):
    # Stand it on its top edge: every surface then overhangs <= ALPHA, so no supports.
    m = m.rotate([180, 0, 0])
    lo = np.asarray(m.bounding_box()[:3])
    hi = np.asarray(m.bounding_box()[3:])
    c = (lo + hi) / 2
    return m.translate([-c[0], -c[1], -lo[2]])


if __name__ == "__main__":
    mounted = build()
    printed = print_orientation(mounted)
    mesh = to_mesh(printed)

    # self-check: one closed solid that fits the bed with no unsupported overhang
    assert mesh.is_watertight and mesh.is_volume, "mesh not solid"
    assert len(mesh.split(only_watertight=True)) == 1, "more than one body"
    ext = mesh.extents
    assert ext[0] <= BED and ext[1] <= BED, f"too big for bed: {ext}"
    ctr = mesh.triangles_center
    down = mesh.face_normals[:, 2] < -0.01
    off_bed = ctr[:, 2] > 0.01
    # screw holes are small horizontal holes that bridge fine; leave them out of the check
    in_hole = np.min([np.hypot(ctr[:, 0] - sx, ctr[:, 2] - STRIP / 2) for sx in (-HOLE_SPACING / 2, HOLE_SPACING / 2)], axis=0) < HEAD / 2 + 0.5
    nz = mesh.face_normals[down & off_bed & ~in_hole][:, 2]
    overhang_from_vertical = math.degrees(math.asin(min(1, -nz.min()))) if len(nz) else 0
    assert overhang_from_vertical <= 45.5, f"overhang {overhang_from_vertical:.1f} deg needs supports"

    mesh.export("padel_holder.stl")
    to_mesh(mounted).export("padel_holder_mounted.stl")
    tape_cm2 = mesh.area_faces[np.abs(mesh.face_normals[:, 1] + 1) < 1e-6].sum() / 100
    print(f"tape area {tape_cm2:.0f} cm2, size {ext.round(1)} mm, volume {mesh.volume / 1000:.1f} cm3, "
          f"~{mesh.volume / 1000 * 1.27:.0f} g PETG solid, worst overhang {overhang_from_vertical:.1f} deg")
