#!/usr/bin/env python3
"""
PlayX Box — parametrické šasi (PODKLAD pro 3D tisk).
CueX Manager program · 2026-07-20 · generováno kódem (manifold3d).

Rozměry Pi 5 a rozteče děr = oficiální mechanika. Položky TODO-VERIFY
se před finálním tiskem přeměří na reálném stacku (Pi 5 + Active Cooler
+ GPIO extender + HiFiBerry DAC2 Pro XLR).

Spuštění:  ~/.venvs/cad/bin/python playx_box_case.py
Výstup:    playx-box-tray.stl · playx-box-lid.stl · playx-box-preview.png
"""
import numpy as np
import trimesh
from manifold3d import Manifold, set_circular_segments

set_circular_segments(64)

# ---------- Pi 5 (oficiální) ----------
PI_L, PI_W = 85.0, 56.0
PI_HOLE_DX, PI_HOLE_DY = 58.0, 49.0
PI_HOLE_OFF = 3.5
PI_HOLE_D = 2.7            # M2.5

# ---------- Stack výšky ----------
STANDOFF_H = 6.0
PI_PCB_T   = 1.4
HAT_GAP    = 19.0          # TODO-VERIFY: GPIO extender + Active Cooler pod HATem
HAT_PCB_T  = 1.6
HAT_CLEAR  = 14.0          # TODO-VERIFY: XLR tělo na DAC2 Pro XLR

# ---------- Šasi ----------
WALL, FLOOR_T, LID_T = 2.4, 2.4, 2.4
CLEAR_XY = 3.0
INNER_L = PI_L + 2*CLEAR_XY + 14   # + prostor u zadního panelu
INNER_W = PI_W + 2*CLEAR_XY
INNER_H = STANDOFF_H + PI_PCB_T + HAT_GAP + HAT_PCB_T + HAT_CLEAR + 4
BOX_L = INNER_L + 2*WALL
BOX_W = INNER_W + 2*WALL
BOX_H = FLOOR_T + INNER_H

# ---------- Zadní panel (y=0): XLR výřezy ----------
XLR_D = 24.0               # Neutrik D-series
XLR_SCREW_D = 3.2
XLR_SCREW_R = 19.8/2
XLR1_X, XLR2_X = 22.0, 50.0     # TODO-VERIFY dle pozice HATu
DMX_X = 78.0                    # XLR DMX (Tinkerforge)
XLR_Z = FLOOR_T + STANDOFF_H + PI_PCB_T + HAT_GAP + 12   # TODO-VERIFY

# ---------- Boční výřezy (Pi hrany) — TODO-VERIFY pozice ----------
SIDE_Z = FLOOR_T + STANDOFF_H + 0.6
USBC = (8.0, 10.0, 4.0)         # x-offset na hraně, šířka, výška
HDMI1 = (24.0, 8.0, 4.5)
HDMI2 = (38.0, 8.0, 4.5)
RJ45 = (6.0, 17.0, 14.0)
USBA = (28.0, 15.0, 16.0)

# ---------- Fan 40 mm (víko) ----------
FAN_HOLE_DX = 32.0
FAN_SCREW_D = 3.2
FAN_X, FAN_Y = BOX_L/2, BOX_W/2 - 4   # TODO-VERIFY: nad SoC/coolerem

BOSS_D, BOSS_HOLE = 7.0, 2.2

def cyl(d, h):
    return Manifold.cylinder(h, d/2, d/2)

def box(x, y, z):
    return Manifold.cube([x, y, z])

def xlr_cutout(x, z):
    """Kruhový výřez + 2 M3 díry pod 45°, osa Y (skrz zadní stěnu)."""
    c = cyl(XLR_D, WALL+2)
    for a in (45, 225):
        sx = XLR_SCREW_R * np.cos(np.radians(a))
        sz = XLR_SCREW_R * np.sin(np.radians(a))
        c += cyl(XLR_SCREW_D, WALL+2).translate([sx, sz, 0])
    return c.rotate([-90, 0, 0]).translate([x, -1, z])

def tray():
    t = box(BOX_L, BOX_W, BOX_H)
    t -= box(INNER_L, INNER_W, INNER_H+1).translate([WALL, WALL, FLOOR_T])
    # zadní panel: 2× XLR audio + XLR DMX
    for x in (XLR1_X, XLR2_X, DMX_X):
        t -= xlr_cutout(x, XLR_Z)
    # boční stěna x=0: USB-C + 2× microHDMI
    for off, w, h in (USBC, HDMI1, HDMI2):
        t -= box(WALL+2, w, h).translate([-1, WALL+CLEAR_XY+off, SIDE_Z])
    # boční stěna x=BOX_L: RJ45 + USB-A
    for off, w, h in (RJ45, USBA):
        t -= box(WALL+2, w, h).translate([BOX_L-WALL-1, WALL+CLEAR_XY+off, SIDE_Z])
    # diagonální větrací sloty (stěna y=BOX_W)
    slot = (cyl(3, WALL+4) + cyl(3, WALL+4).translate([0, 18, 0]))
    slot = Manifold.hull_points  # not used; build hull via convex_hull of two cyls
    for i in range(6):
        s = (cyl(3, WALL+4).translate([0, 0, 0]) + cyl(3, WALL+4).translate([0, 18, 0])).hull()
        s = s.rotate([90, 25, 0]).translate([BOX_L-58+i*8, BOX_W+1, FLOOR_T+8])
        t -= s
    # sloupky Pi
    for dx in (0, PI_HOLE_DX):
        for dy in (0, PI_HOLE_DY):
            px = WALL + CLEAR_XY + PI_HOLE_OFF + dx
            py = WALL + CLEAR_XY + PI_HOLE_OFF + dy
            st = cyl(6, STANDOFF_H) - cyl(PI_HOLE_D, STANDOFF_H).translate([0, 0, 1])
            t += st.translate([px, py, FLOOR_T])
    # rohové bosses pro víko
    for bx in (WALL+BOSS_D/2, BOX_L-WALL-BOSS_D/2):
        for by in (WALL+BOSS_D/2, BOX_W-WALL-BOSS_D/2):
            b = cyl(BOSS_D, INNER_H) - cyl(BOSS_HOLE, 11).translate([0, 0, INNER_H-10])
            t += b.translate([bx, by, FLOOR_T])
    return t

def lid():
    l = box(BOX_L, BOX_W, LID_T)
    # fan mřížka: 3 mezikruží
    for r in (6, 11, 16):
        ring = cyl(2*(r+1.2), LID_T+2) - cyl(2*(r-1.2), LID_T+4).translate([0, 0, -1])
        l -= ring.translate([FAN_X, FAN_Y, -1])
    for sx in (-FAN_HOLE_DX/2, FAN_HOLE_DX/2):
        for sy in (-FAN_HOLE_DX/2, FAN_HOLE_DX/2):
            l -= cyl(FAN_SCREW_D, LID_T+2).translate([FAN_X+sx, FAN_Y+sy, -1])
    # rohové šrouby
    for bx in (WALL+BOSS_D/2, BOX_L-WALL-BOSS_D/2):
        for by in (WALL+BOSS_D/2, BOX_W-WALL-BOSS_D/2):
            l -= cyl(2.8, LID_T+2).translate([bx, by, -1])
    return l

def to_trimesh(m):
    mesh = m.to_mesh()
    return trimesh.Trimesh(vertices=np.asarray(mesh.vert_properties)[:, :3],
                           faces=np.asarray(mesh.tri_verts))

if __name__ == "__main__":
    t, l = tray(), lid()
    tm_t, tm_l = to_trimesh(t), to_trimesh(l)
    tm_t.export("playx-box-tray.stl")
    tm_l.export("playx-box-lid.stl")

    # PNG náhled (matplotlib, dvě části vedle sebe)
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d.art3d import Poly3DCollection
    fig = plt.figure(figsize=(14, 7), facecolor="#111")
    for i, (tm, title) in enumerate(((tm_t, "Vana (tray)"), (tm_l, "Víko (lid) + fan mřížka"))):
        ax = fig.add_subplot(1, 2, i+1, projection="3d", facecolor="#111")
        polys = tm.vertices[tm.faces]
        pc = Poly3DCollection(polys, alpha=1.0, facecolor="#2b2b31", edgecolor="#FF6A00", linewidths=0.05)
        ax.add_collection3d(pc)
        lim = max(BOX_L, BOX_W, BOX_H)
        ax.set_xlim(0, lim); ax.set_ylim(0, lim); ax.set_zlim(0, lim)
        ax.view_init(elev=28, azim=-55)
        ax.set_axis_off()
        ax.set_title(title, color="#e8e6e1")
    fig.suptitle(f"PlayX Box case — podklad · vnější {BOX_L:.0f} × {BOX_W:.0f} × {BOX_H+LID_T:.0f} mm",
                 color="#FF6A00")
    fig.savefig("playx-box-preview.png", dpi=110, bbox_inches="tight", facecolor="#111")
    print(f"OK: tray {len(tm_t.faces)} faces (watertight={tm_t.is_watertight}), "
          f"lid {len(tm_l.faces)} faces (watertight={tm_l.is_watertight}); "
          f"box {BOX_L:.0f}×{BOX_W:.0f}×{BOX_H+LID_T:.0f} mm")
