# Корпус адаптера CW-ключа під плату YD-RP2040 (51 x 21 мм) + гніздо 3.5 мм.
# Запуск: freecadcmd case.py   ->  case_base.stl, case_lid.stl, case.step
# Усі розміри в мм. Під інше гніздо міняйте блок «ГНІЗДО» і перезапускайте.
import os
import FreeCAD as App
import Part
import MeshPart

V = App.Vector
OUT = os.path.dirname(os.path.abspath(__file__))

# ---------- ПЛАТА ----------
PCB_L = 53.8      # довжина текстоліту без USB: на штангенциркулі 53.5 + запас 0.3
PCB_W = 23.3      # ширина: на штангенциркулі 23.2 + запас 0.1 (спершу помилково було 21)
PCB_T = 1.6       # товщина PCB (виміряно)
UNDER = 3.0       # простір під платою (пайка, дроти)
COMP_H = 4.0      # найвищий елемент над платою (USB-C 3.2 мм, кнопки)
USB_H = 3.2       # висота USB-C роз'єму над платою
GAP = 0.25        # зазор плата-стінка з кожного боку
USB_OVERHANG = 0.3  # наскільки USB-C виступає за край плати
BOOT_X, BOOT_Y = 20.8, 6.7  # центр кнопки BOOT від USB-краю плати / від краю з GP0..GP15 (за сіткою 2.54 мм)

# ---------- ГНІЗДО ----------
# Гніздо під пайку в плату (зелене, від звукової карти). Лежить у ложементі ВИВОДАМИ ДОГОРИ,
# носик виходить у отвір торцевої стінки; дроти паяються до виводів зверху.
JACK_W = 12.0       # ширина корпусу гнізда (виміряно)
JACK_BODY_L = 12.3  # довжина корпусу без носика (виміряно)
JACK_H = 5.0        # товщина корпусу без виводів (виміряно)
JACK_PIN = 3.5      # довжина виводів (оцінка)
JACK_HOLE_D = 6.4   # отвір у стінці: діаметр носика 6.0 (ОЦІНКА — переміряйте) + 0.4
JACK_AXIS = 2.5     # від плоскої грані без виводів до осі носика (ОЦІНКА: посередині товщини)
JACK_PED = 2.5      # висота ложемента над дном (щоб штекер не лежав на столі)
JACK_FIT = 0.2      # зазор гнізда в ложементі з кожного боку
WIRE_SPACE = 3.7    # місце під дроти між платою і гніздом

# ---------- КОРПУС ----------
WALL = 2.0
FLOOR = 2.0
LID_T = 2.0
LIP_H = 3.0       # висота бортика кришки
LIP_T = 1.2       # товщина бортика
TOL = 0.2         # зазор посадки кришки
CORNER_R = 2.0
USB_CUT_W, USB_CUT_H, USB_CUT_R = 12.5, 7.0, 1.5  # виріз під штекер з обтиском
BOOT_HOLE_D = 3.0
SNAP_R, SNAP_LEN = 0.5, 20.0  # защіпки на бортику

# ---------- ЛОГОТИП НА КРИШЦІ (заглиблення під заливку фарбою) ----------
LOGO_SVG = "logo.svg"   # контури potrace; порожній рядок = без логотипа
LOGO_SIZE = 28.0        # сторона квадрата логотипа (доріжки ~1.0 мм, стінки між ними ~0.9 мм)
LOGO_MARGIN = 2.5       # від логотипа до краю кришки
LOGO_DEPTH = 0.6        # глибина заглиблення
LOGO_FROM_JACK = 16.5   # центр логотипа від торця з гніздом
TEXT = "UR3PKI"         # напис ліворуч від логотипа; порожній рядок = без напису
TEXT_FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
TEXT_W = 38.0           # ширина напису
TEXT_GAP = 2.5          # відступ між написом і логотипом
TEXT_COLOR = "#1f2328"
LOGO_COLORS = ["#da1f0b", "#4a90e2", "#4a90e2", "#da1f0b", "#4a90e2", "#4a90e2", "#f5b82e", "#f5b82e", "#f5b82e"]

# ---------- ПОХІДНІ ----------
IN_W = max(PCB_W + 2 * GAP, JACK_W + 2 * JACK_FIT + 3.0, LOGO_SIZE + 2 * LOGO_MARGIN - 2 * WALL)
IN_L = USB_OVERHANG + GAP + PCB_L + 2.5 + WIRE_SPACE + JACK_BODY_L
IN_H = max(UNDER + PCB_T + COMP_H + LIP_H + 0.5, JACK_PED + JACK_H + JACK_PIN + 1.0)
OUT_L, OUT_W = IN_L + 2 * WALL, IN_W + 2 * WALL
BASE_H = FLOOR + IN_H

PX0 = WALL + USB_OVERHANG + GAP          # початок плати по X
PX1 = PX0 + PCB_L
PY0 = WALL + GAP                         # край плати з боку GP0..GP15 (плата притиснута до цієї стінки,
                                         # щоб отвір BOOT не потрапив під напис)
PY1 = PY0 + PCB_W
PZ0 = FLOOR + UNDER                      # низ плати
PZ1 = PZ0 + PCB_T                        # верх плати
YC = OUT_W / 2
YB = PY0 + PCB_W / 2                     # вісь плати (USB)
RAIL = PY1 + GAP + 1.5                   # зовнішній край напрямної вздовж вільного боку плати

assert PZ1 + COMP_H < BASE_H - LIP_H, "бортик кришки впирається в компоненти"


def box(x0, y0, z0, x1, y1, z1):
    return Part.makeBox(x1 - x0, y1 - y0, z1 - z0, V(x0, y0, z0))


def rounded_box(l, w, h, r, z0=0.0):
    b = Part.makeBox(l, w, h, V(0, 0, z0))
    vert = [e for e in b.Edges if abs(e.Vertexes[0].Point.z - e.Vertexes[1].Point.z) > 1e-6]
    return b.makeFillet(r, vert)


def cyl_x(d, x0, length, y, z):
    return Part.makeCylinder(d / 2, length, V(x0, y, z), V(1, 0, 0))


def slot_x(w, h, r, x0, length, yc, zc):
    """Прямокутник з заокругленими кутами, видавлений по X."""
    core = box(x0, yc - w / 2 + r, zc - h / 2, x0 + length, yc + w / 2 - r, zc + h / 2)
    core = core.fuse(box(x0, yc - w / 2, zc - h / 2 + r, x0 + length, yc + w / 2, zc + h / 2 - r))
    for sy in (-1, 1):
        for sz in (-1, 1):
            core = core.fuse(cyl_x(2 * r, x0, length, yc + sy * (w / 2 - r), zc + sz * (h / 2 - r)))
    return core.removeSplitter()


# ================= ОСНОВА =================
base = rounded_box(OUT_L, OUT_W, BASE_H, CORNER_R)
base = base.cut(box(WALL, WALL, FLOOR, OUT_L - WALL, OUT_W - WALL, BASE_H + 1))

# виріз USB-C у торці x=0
usb_zc = PZ1 + USB_H / 2
base = base.cut(slot_x(USB_CUT_W, USB_CUT_H, USB_CUT_R, -1, WALL + 2, YB, usb_zc))

# отвір під гніздо в протилежному торці
jack_zc = FLOOR + JACK_PED + JACK_AXIS
base = base.cut(cyl_x(JACK_HOLE_D, OUT_L - WALL - 1, WALL + 2, YC, jack_zc))

# ложемент гнізда: постамент, бокові стінки й задній упор (штекер тисне на нього)
jx1 = OUT_L - WALL                      # внутрішня грань торцевої стінки
jx0 = jx1 - JACK_BODY_L - JACK_FIT      # задня грань гнізда
jw = JACK_W / 2 + JACK_FIT
cradle = box(jx0 - 1.5, YC - jw - 1.5, FLOOR - 0.01, jx1 + 0.01, YC + jw + 1.5, FLOOR + JACK_PED + JACK_H - 0.5)
cradle = cradle.cut(box(jx0, YC - jw, FLOOR + JACK_PED, jx1 + 1, YC + jw, BASE_H))
base = base.fuse(cradle)

# опори під довгими краями плати (подалі від GP14/GP15/GND біля дальнього торця)
for (ya, yb) in ((WALL, PY0 + 0.8), (PY1 - 0.8, RAIL)):
    base = base.fuse(box(PX0 + 5, ya, FLOOR - 0.01, PX0 + 38, yb, PZ0))
    # упори за дальнім торцем плати
    base = base.fuse(box(PX1 + 0.2, ya - (0.0 if ya == WALL else 1.0), FLOOR - 0.01,
                         PX1 + 2.2, yb + (1.0 if ya == WALL else 0.0), PZ1 + 0.5))
# напрямна вздовж вільного боку плати (не дає платі з'їхати вбік)
base = base.fuse(box(PX0 + 5, PY1 + GAP, FLOOR - 0.01, PX0 + 38, RAIL, PZ1 + 1.0))
# опора під USB-торцем (центр, під роз'ємом)
base = base.fuse(box(PX0 + 1, YB - 5, FLOOR - 0.01, PX0 + 4, YB + 5, PZ0 - 1.0))  # нижче плати: під роз'ємом стирчать ніжки

# пази для защіпок на внутрішніх довгих стінках
snap_z = BASE_H - LIP_H / 2
for y in (WALL, OUT_W - WALL):
    base = base.cut(cyl_x(2 * (SNAP_R + 0.1), OUT_L / 2 - (SNAP_LEN + 4) / 2, SNAP_LEN + 4, y, snap_z))

base = base.removeSplitter()

# ================= КРИШКА (в зборі, z від BASE_H) =================
lid = rounded_box(OUT_L, OUT_W, LID_T, CORNER_R, z0=BASE_H)
li0, lo = WALL + TOL, LIP_T
lip_z0 = BASE_H - LIP_H
# бортик вздовж довгих стінок і на USB-торці; на торці з гніздом бортика немає
lip = box(li0, li0, lip_z0, OUT_L - WALL - TOL, li0 + lo, BASE_H + 0.01)
lip = lip.fuse(box(li0, OUT_W - li0 - lo, lip_z0, OUT_L - WALL - TOL, OUT_W - li0, BASE_H + 0.01))
lip = lip.fuse(box(li0, li0, lip_z0, li0 + lo, OUT_W - li0, BASE_H + 0.01))
# защіпки
for y in (li0, OUT_W - li0):
    lip = lip.fuse(cyl_x(2 * SNAP_R, OUT_L / 2 - SNAP_LEN / 2, SNAP_LEN, y, snap_z))
# притискачі плати зверху (ті ж ділянки, що й опори знизу)
lip = lip.fuse(box(PX0 + 12, li0, PZ1 + 0.1, PX0 + 32, li0 + LIP_T, BASE_H + 0.01))
lip = lip.fuse(box(PX0 + 12, PY1 - LIP_T, PZ1 + 0.1, PX0 + 32, PY1 + 0.05, BASE_H + 0.01))
lid = lid.fuse(lip)
# отвір до кнопки BOOT (натискати скріпкою)
lid = lid.cut(Part.makeCylinder(BOOT_HOLE_D / 2, LID_T + 2, V(PX0 + BOOT_X, PY0 + BOOT_Y, BASE_H - 1)))
lid = lid.removeSplitter()

# логотип: кожен контур SVG -> окрема кишенька на верхній площині кришки
import re


def svg_outlines(fname):
    """Контури potrace (M/c/l/z, один підконтур на path) -> списки точок у одиницях SVG."""
    out = []
    for d in re.findall(r'<path d="([^"]+)"', open(fname, encoding="utf-8").read()):
        toks = re.findall(r"[MmCcLlZz]|-?\d+\.?\d*", d)
        i, cmd, x, y, pts = 0, "", 0.0, 0.0, []
        num = lambda k: [float(t) for t in toks[i:i + k]]
        while i < len(toks):
            if toks[i].isalpha():
                cmd = toks[i]; i += 1
                continue
            if cmd in "Mm":
                dx, dy = num(2); i += 2
                x, y = (dx, dy) if cmd == "M" else (x + dx, y + dy)
                pts.append((x, y)); cmd = "l" if cmd == "m" else "L"
            elif cmd in "Ll":
                dx, dy = num(2); i += 2
                x, y = (dx, dy) if cmd == "L" else (x + dx, y + dy)
                pts.append((x, y))
            elif cmd in "Cc":
                c = num(6); i += 6
                if cmd == "c":
                    c = [c[0] + x, c[1] + y, c[2] + x, c[3] + y, c[4] + x, c[5] + y]
                for k in range(1, 9):
                    t = k / 8.0; u = 1 - t
                    pts.append((u**3 * x + 3 * u * u * t * c[0] + 3 * u * t * t * c[2] + t**3 * c[4],
                                u**3 * y + 3 * u * u * t * c[1] + 3 * u * t * t * c[3] + t**3 * c[5]))
                x, y = c[4], c[5]
        out.append(pts)
    return out


paint = []  # (тіло заливки, колір) — лише для 3D-перегляду
if LOGO_SVG:
    k = LOGO_SIZE / 5120.0
    cx, top = OUT_L - LOGO_FROM_JACK, BASE_H + LID_T
    for n, pts in enumerate(svg_outlines(os.path.join(OUT, LOGO_SVG))):
        p3 = []
        for (x, y) in pts:
            v = V(cx + (x - 2560) * k, YC + (y - 2560) * k, top - LOGO_DEPTH)
            if not p3 or (v - p3[-1]).Length > 0.02:
                p3.append(v)
        if (p3[0] - p3[-1]).Length < 0.02:
            p3.pop()
        face = Part.Face(Part.makePolygon(p3 + [p3[0]]))
        lid = lid.cut(face.extrude(V(0, 0, LOGO_DEPTH + 1)))
        paint.append((face.extrude(V(0, 0, LOGO_DEPTH / 2)), LOGO_COLORS[n % len(LOGO_COLORS)]))


def text_faces(height):
    faces = []
    for char in Part.makeWireString(TEXT, os.path.dirname(TEXT_FONT) + "/", os.path.basename(TEXT_FONT), height, 0):
        ws = sorted(char, key=lambda w: -Part.Face(w).Area)
        if not ws:
            continue
        f = Part.Face(ws[0])
        for hole in ws[1:]:
            f = f.cut(Part.Face(hole))
        faces.append(f)
    return Part.makeCompound(faces)


if TEXT:
    h0 = 10.0
    h0 *= TEXT_W / text_faces(h0).BoundBox.XLength
    txt = text_faces(h0)
    bb = txt.BoundBox
    x_right = (OUT_L - LOGO_FROM_JACK - LOGO_SIZE / 2 - TEXT_GAP) if LOGO_SVG else (OUT_L + TEXT_W) / 2
    txt.translate(V(x_right - bb.XMax, YC - (bb.YMin + bb.YMax) / 2, BASE_H + LID_T - LOGO_DEPTH))
    assert txt.BoundBox.XMin > WALL + 1.0, "напис виходить за край кришки — зменшіть TEXT_W"
    assert txt.BoundBox.YMin > PY0 + BOOT_Y + BOOT_HOLE_D / 2 + 1.5, "напис налазить на отвір BOOT — зменшіть TEXT_W"
    lid = lid.cut(txt.extrude(V(0, 0, LOGO_DEPTH + 1)))
    paint.append((txt.extrude(V(0, 0, LOGO_DEPTH / 2)), TEXT_COLOR))
    print("TEXT height %.1f mm, x %.1f..%.1f" % (bb.YLength, txt.BoundBox.XMin, txt.BoundBox.XMax))

assembly = Part.makeCompound([base, lid])

# кришку для друку перевертаємо площиною на стіл
lid_print = lid.copy()
lid_print.rotate(V(0, 0, 0), V(1, 0, 0), 180)
lid_print.translate(V(0, OUT_W, BASE_H + LID_T))
lid_print.translate(V(0, OUT_W + 10, 0))


def write_stl(shape, name):
    mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.02, AngularDeflection=0.15, Relative=False)
    mesh.write(os.path.join(OUT, name))


for s, n in ((base, "base"), (lid, "lid")):
    assert s.isValid() and len(s.Solids) == 1, n
write_stl(base, "case_base.stl")
write_stl(lid_print, "case_lid.stl")
assembly.exportStep(os.path.join(OUT, "case.step"))
print("OUTER %.1f x %.1f x %.1f mm; volume base %.0f lid %.0f mm3"
      % (OUT_L, OUT_W, BASE_H + LID_T, base.Volume, lid.Volume))

# ---------- 3D-модель у trainer.html (корпус у зборі) ----------
import base64, json, re, struct


def soup(shape):
    m = MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.05, AngularDeflection=0.3, Relative=False)
    pts, tris = m.Topology
    flat = [c for t in tris for i in t for c in (pts[i].x, pts[i].y, pts[i].z)]
    return base64.b64encode(struct.pack("<%df" % len(flat), *flat)).decode()


html_path = os.path.join(OUT, "..", "trainer.html")
if os.path.exists(html_path):
    html = open(html_path, encoding="utf-8").read()
    block = ('<!--CASE-DATA--><script id="caseData" type="application/json">'
             + json.dumps({"base": soup(base), "lid": soup(lid),
                           "paint": [[soup(b), [int(c[i:i + 2], 16) / 255 for i in (1, 3, 5)]] for b, c in paint]}) + "</script><!--/CASE-DATA-->")
    html, n = re.subn(r"<!--CASE-DATA-->.*?<!--/CASE-DATA-->", lambda _: block, html, flags=re.S)
    if n:
        open(html_path, "w", encoding="utf-8").write(html)
        print("trainer.html: 3D-модель оновлено")
