"""Řada zásuvek 200 x 200 x 70 mm zavěšených pod policí na rybinových lištách - pro FDM tisk (Bambu Lab P2S, 256^3 mm).

Spuštění:  blender -b -P build_supl.py   (potom make_3mf.py)
Jednotky: 1 Blender unit = 1 mm.  X = šířka, Y = hloubka (čelo zásuvky v Y=0, vysouvá se do -Y),
Z = výška (spodní strana police v Z=0, zásuvka visí do -Z).

Princip
- Zásuvky stojí vedle sebe s roztečí P = 200 + mezera. Celé vedení (lišta + klíč) se vejde dovnitř šířky zásuvky:
  klíč (rybina) sedí na žebru uvnitř horního okraje boční stěny, lišta nad ním je přišroubovaná k polici (4 šrouby).
- Čelo je vytažené až k polici (0.8 mm) = žádná škvíra nad zásuvkou; žlábkování na čele i madle.
- Aretace zavřeno: 2 páry magnetů 5 x 2 mm schované v žebru šuplíku a ve spodku lišty (nic nevyčnívá, nic se nelepí na polici).
- Přední zarážka lišty se zajišťuje kolíkem z filamentu 1.75 mm (bez šroubů).
- Žádný díl nemá převis > ~45°, tiskne se bez podpor. STL jsou v tiskové orientaci ve složce stl/.
"""
import bpy, bmesh, os, math, glob
from mathutils import Vector, Matrix

OUT = os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals() else r"C:\AI\3d modely\krabička"

# ---------- parametry ----------
W, D, H = 200.0, 200.0, 70.0          # vnější rozměr těla zásuvky
GAP = 2.0                             # mezera mezi sousedními zásuvkami
P = W + GAP                           # rozteč
N = 3                                 # počet zásuvek v náhledu
WALL, FRONT, BOTTOM = 2.4, 5.0, 2.4   # stěny / čelo / dno (2.4 mm = 6 průchodů trysky 0.4)
CL = 0.4                              # vůle rybiny na stranu (u P2S 0.3-0.5 mm) -> po zkoušce test_vule_* upravit

# rybina: klíč na zásuvce
KN, KS, KF = 4.5, 4.0, 2.0            # krk (svisle) / 53° rozšíření / plochá hlava
KH = KN + KS + KF                     # výška klíče
HW_N, HW_H = 2.5, 5.5                 # poloviční šířka krku / hlavy
FL_T = 4.6                            # tloušťka police lišty (= strop drážky)
ZR = -(FL_T + KH + CL)                # horní hrana těla zásuvky = -15.5
ZB = ZR - H                           # dno zásuvky
FZ = -0.8                             # čelo sahá skoro k polici
RAIL_B = -(ZR + 1.2)                  # spodek lišty 1.2 mm nad okrajem zásuvky (hloubka pod policí)
XC = 90.0                             # osa lišty/klíče od středu zásuvky (uvnitř šířky zásuvky)
BLK_HW = 10.0                         # polovina šířky bloku lišty

RAIL_Y0 = FRONT + 0.5                 # lišta začíná hned za čelem
KEY_LEN = 76.0                        # délka klíče: delší = pevnější, kratší = delší výsuv
KEY_Y1 = 197.0
KEY_Y0 = KEY_Y1 - KEY_LEN
GROOVE_END = KEY_Y1 + 2.0             # drážka vzadu uzavřená (zarážka při zasunutí)
RAIL_L = GROOVE_END + 4.0 - RAIL_Y0   # lišta 197.5 mm
STOP_L = 8.0                          # délka přední zarážky
TRAVEL = KEY_Y0 - (RAIL_Y0 + STOP_L)  # výsuv zásuvky
SHELF_D, SHELF_T = 215.0, 18.0
PIN_D = 1.9                           # díra pro kolík z filamentu 1.75 mm
MAG_D, MAG_H = 5.2, 2.2               # kapsa pro magnet 5 x 2 mm (v žebru šuplíku a ve spodku lišty, proti sobě)
MAG_X = XC + 6.5                      # osa magnetu od středu šuplíku (nad vnější stěnou)
MAG_Y = KEY_Y1 - 10.0                 # poloha magnetu od čela v zavřené poloze
# variabilní vnitřek: mřížka drážek na přepážky (egg-crate, přepážky se do sebe zasouvají zářezy)
TR_Y = tuple(29.0 + 24.0 * k for k in range(7))   # polohy příčných přepážek (od čela), drážky v bočních stěnách
LG_X = (-66.0, -33.0, 0.0, 33.0, 66.0)            # polohy podélných přepážek (od osy), drážky v čele a zadní stěně
DIV_T = 1.6                           # tloušťka přepážky
NOTCH = 25.5                          # hloubka zářezů (příčné shora, podélné zdola)
# vložné přihrádky (krabičky): modulová mřížka 4 x 4 po BM mm mezi hroty žeber ve stěnách (alternativa k přepážkám)
BM, BG, BT = 46.6, 1.0, 1.2           # modul / mezera mezi krabičkami / stěna a dno krabičky
BHF, BHL = 45.0, 22.0                 # výška plná / nízká
BIN_X0 = -2 * BM
BIN_Y0 = FRONT + 3.0 + (D - WALL - FRONT - 6.0 - 4 * BM) / 2
DIV_TOP = ZR - 17.0                   # horní hrana přepážek

# ---------- scéna ----------
bpy.ops.wm.read_factory_settings(use_empty=True)
sc = bpy.context.scene
sc.unit_settings.system = 'METRIC'
sc.unit_settings.scale_length = 0.001
sc.unit_settings.length_unit = 'MILLIMETERS'

def coll(name):
    c = bpy.data.collections.new(name)
    sc.collection.children.link(c)
    return c
c_drawer, c_rail, c_shelf, c_tmp = coll("Supl"), coll("Listy a zarazky"), coll("Police (kontext)"), coll("tmp")

def mat(name, col, rough=0.5):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    b = m.node_tree.nodes["Principled BSDF"]
    b.inputs["Base Color"].default_value = (*col, 1)
    b.inputs["Roughness"].default_value = rough
    return m
M_DRAWERS = [mat("Supl_%d" % i, c, 0.45) for i, c in enumerate(((0.88, 0.88, 0.85), (0.25, 0.45, 0.75), (0.85, 0.85, 0.8)))]
M_RAIL = mat("Lista_plast", (0.9, 0.35, 0.08), 0.45)
M_WOOD = mat("Police_drevo", (0.55, 0.38, 0.2), 0.6)
M_JIG = mat("Pripravek", (0.2, 0.7, 0.3), 0.5)
M_DIV = mat("Predelka", (0.35, 0.65, 0.9), 0.5)
M_BIN = mat("Prihradka", (0.95, 0.75, 0.2), 0.5)

# ---------- pomocné funkce ----------
def new_obj(name, bm, c, m=None):
    me = bpy.data.meshes.new(name)
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    bm.to_mesh(me); bm.free()
    o = bpy.data.objects.new(name, me)
    c.objects.link(o)
    if m:
        o.data.materials.append(m)
    return o

def _extrude(bm, pts3, vec):
    f = bm.faces.new([bm.verts.new(p) for p in pts3])
    r = bmesh.ops.extrude_face_region(bm, geom=[f])
    bmesh.ops.translate(bm, vec=vec, verts=[v for v in r["geom"] if isinstance(v, bmesh.types.BMVert)])
    return bm

def prism_xz(pts, y0, y1, xoff=0.0, bm=None):         # (x, z) vytažené ve směru Y
    return _extrude(bm or bmesh.new(), [(x + xoff, y0, z) for x, z in pts], (0, y1 - y0, 0))

def prism_yz(pts, x0, x1, bm=None):                   # (y, z) vytažené ve směru X
    return _extrude(bm or bmesh.new(), [(x0, y, z) for y, z in pts], (x1 - x0, 0, 0))

def cuboid(x0, x1, y0, y1, z0, z1, bm=None):
    bm = bm or bmesh.new()
    vs = bmesh.ops.create_cube(bm, size=1.0)["verts"]
    for v in vs:
        v.co = Vector((x0 if v.co.x < 0 else x1, y0 if v.co.y < 0 else y1, z0 if v.co.z < 0 else z1))
    return bm

def rrect(x0, x1, y0, y1, r, seg=6):
    pts = []
    for cx, cy, a0 in ((x1 - r, y1 - r, 0), (x0 + r, y1 - r, 90), (x0 + r, y0 + r, 180), (x1 - r, y0 + r, 270)):
        for i in range(seg + 1):
            a = math.radians(a0 + 90 * i / seg)
            pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
    return pts

def prism_z(pts_xy, z0, z1, chamfer_bottom=0.0):
    bm = _extrude(bmesh.new(), [(x, y, z0) for x, y in pts_xy], (0, 0, z1 - z0))
    if chamfer_bottom:
        edges = [e for e in bm.edges if all(abs(v.co.z - z0) < 1e-6 for v in e.verts)]
        bmesh.ops.bevel(bm, geom=edges, offset=chamfer_bottom, segments=1, affect='EDGES')
    return bm

def add_cone(bm, r1, r2, depth, loc, rot=None, seg=32):
    vs = bmesh.ops.create_cone(bm, cap_ends=True, segments=seg, radius1=r1, radius2=r2, depth=depth)["verts"]
    if rot is not None:
        bmesh.ops.rotate(bm, verts=vs, cent=(0, 0, 0), matrix=rot)
    bmesh.ops.translate(bm, vec=loc, verts=vs)

def boolean(target, cutter, op='DIFFERENCE'):
    md = target.modifiers.new("b", 'BOOLEAN')
    md.operation, md.object, md.solver = op, cutter, 'EXACT'
    for o in bpy.context.selected_objects:
        o.select_set(False)
    target.select_set(True)
    bpy.context.view_layer.objects.active = target
    bpy.ops.object.modifier_apply(modifier=md.name)
    bpy.data.objects.remove(cutter, do_unlink=True)

def mirror(pts, s):
    return [(s * x, z) for x, z in pts]

RX = Matrix.Rotation(math.pi / 2, 3, 'X')
RY = Matrix.Rotation(math.pi / 2, 3, 'Y')

# ---------- profily (X relativně k ose lišty/klíče, Z vůči polici) ----------
KEY = [(-HW_N, ZR - 2), (HW_N, ZR - 2), (HW_N, ZR + KN), (HW_H, ZR + KN + KS), (HW_H, ZR + KH),
       (-HW_H, ZR + KH), (-HW_H, ZR + KN + KS), (-HW_N, ZR + KN)]
def groove_pts(cl):                    # drážka v liště = klíč + vůle cl (45°/53° plochy rovnoběžně)
    return [(HW_N + cl, ZR - 1), (HW_N + cl, ZR + KN), (HW_H + cl, ZR + KN + KS), (HW_H + cl, ZR + KH + cl),
            (-HW_H - cl, ZR + KH + cl), (-HW_H - cl, ZR + KN + KS), (-HW_N - cl, ZR + KN), (-HW_N - cl, ZR - 1)]
Z_RB = ZR + 1.2
PL = [(HW_N + CL - .15, Z_RB), (HW_N + CL - .15, ZR + KN), (HW_H + CL - .15, ZR + KN + KS), (HW_H + CL - .15, ZR + KH + CL - .15),
      (-HW_H - CL + .15, ZR + KH + CL - .15), (-HW_H - CL + .15, ZR + KN + KS), (-HW_N - CL + .15, ZR + KN), (-HW_N - CL + .15, Z_RB)]
RAIL_OUT = [(-BLK_HW, 0), (BLK_HW, 0), (BLK_HW, -RAIL_B), (-BLK_HW, -RAIL_B)]
SCREWS = [(-21.0, 12.0), (-21.0, RAIL_L - 15.0)]   # (x rel., y od začátku lišty): jedno ucho se šroubem na každém konci lišty
STOP_HOLE_Z, STOP_HOLE_Y = ZR + KN + 1.5, RAIL_Y0 + STOP_L / 2

def make_rail(name, s, y_len=RAIL_L, holes=True, stop_hole=True, groove_end=GROOVE_END, c=c_rail, xoff=None, cl=CL, dimples=0):
    xoff = s * XC if xoff is None else xoff
    r = new_obj(name, prism_xz(mirror(RAIL_OUT, s), RAIL_Y0, RAIL_Y0 + y_len, xoff), c, M_RAIL)
    if holes:    # uši pro šrouby místo celé příruby (méně materiálu)
        ears = bmesh.new()
        for x, y in SCREWS:
            xa, xb = sorted((xoff + s * (x - 7.0), xoff + s * (BLK_HW - 0.5)))
            cuboid(xa, xb, RAIL_Y0 + y - 9.0, RAIL_Y0 + y + 9.0, -FL_T, 0.0, ears)
        boolean(r, new_obj("ears", ears, c_tmp), 'UNION')
    cuts = [prism_xz(groove_pts(cl), RAIL_Y0 - 1, groove_end, xoff)]
    if holes:    # průchozí M4 a zahloubení pro kuželovou hlavu - zvlášť, aby se řezací tělesa nepřekrývala
        cyl, cone = bmesh.new(), bmesh.new()
        for x, y in SCREWS:
            add_cone(cyl, 2.2, 2.2, 6.0, (xoff + s * x, RAIL_Y0 + y, -2.0))
            add_cone(cone, 4.4, 2.2, 2.2, (xoff + s * x, RAIL_Y0 + y, -FL_T + 1.0))
        cuts += [cyl, cone]
    if stop_hole:  # díra pro kolík z filamentu 1.75 mm (oběma boky lišty -> stejná zarážka pro levou i pravou)
        bm = bmesh.new()
        add_cone(bm, PIN_D / 2, PIN_D / 2, 2 * BLK_HW + 6, (xoff, STOP_HOLE_Y, STOP_HOLE_Z), RY, 16)
        cuts.append(bm)
    if holes:    # kapsa pro magnet ve spodku lišty (vnější okraj drážky), proti magnetu v žebru šuplíku
        bm = bmesh.new()
        add_cone(bm, MAG_D / 2, MAG_D / 2, MAG_H + 0.4, (xoff + s * 6.5, MAG_Y, Z_RB + MAG_H / 2 - 0.2), None, 28)
        cuts.append(bm)
    if dimples:  # označení zkušebních kousků (počet důlků = varianta vůle)
        bm = bmesh.new()
        for i in range(dimples):
            add_cone(bm, 1.6, 1.6, 3.0, (xoff, RAIL_Y0 + 7 + 8 * i, -0.5), None, 16)
        cuts.append(bm)
    for cut in cuts:
        boolean(r, new_obj("cut", cut, c_tmp))
    return r

def make_plug(name, xoff):
    p = new_obj(name, prism_xz(PL, RAIL_Y0, RAIL_Y0 + STOP_L, xoff), c_rail, M_RAIL)
    bm = bmesh.new()
    add_cone(bm, PIN_D / 2, PIN_D / 2, 20.0, (xoff, STOP_HOLE_Y, STOP_HOLE_Z), RY, 16)
    boolean(p, new_obj("pin", bm, c_tmp))
    return p

# ---------- zásuvka (jeden díl) ----------
body = new_obj("Supl", prism_z(rrect(-W / 2, W / 2, 0, D, 3.0), ZB, FZ, chamfer_bottom=1.0), c_drawer, M_DRAWERS[0])
boolean(body, new_obj("cav", prism_z(rrect(-W / 2 + WALL, W / 2 - WALL, FRONT, D - WALL, 1.5), ZB + BOTTOM, FZ + 5), c_tmp))
boolean(body, new_obj("top", prism_z([(-W / 2 - 1, FRONT), (W / 2 + 1, FRONT), (W / 2 + 1, D + 1), (-W / 2 - 1, D + 1)], ZR, FZ + 2), c_tmp))

PZ = ZB + H / 2                                   # výška madla na čele
HANDLE = True                                     # madlo (False = jen průchozí otvor v čele)
HL, HLEG, HBAR, HGAP, HH = 100.0, 12.0, 12.0, 14.0, 14.0   # délka, šířka nohy, tloušťka příčky, mezera pro prsty, výška
HX = HL / 2 - HLEG / 2                            # osa nohy madla (x = ±44)
if HANDLE:      # dva průchozí otvory pro šrouby M4 zevnitř šuplíku
    bm = bmesh.new()
    for x in (-HX, HX):
        add_cone(bm, 2.25, 2.25, FRONT + 2, (x, FRONT / 2, PZ), RX, 24)
    boolean(body, new_obj("sr", bm, c_tmp))
else:           # úchop v čele: průchozí otvor 90 x 21 mm, strop pod 45° (bez podpor)
    boolean(body, new_obj("pk", prism_yz([(-1, PZ - 10), (FRONT + 1, PZ - 10), (FRONT + 1, PZ + 3), (-1, PZ + 11)], -45, 45), c_tmp))

# strukturované zdobení čela: svislé žlábky (rozteč 4 mm, rádius 1.3, hloubka 1 mm); hladký pruh kolem madla
# a hladké pole pro štítek pod madlem
FLUTES = True
LAB_Z0, LAB_Z1 = ZB + 8.0, ZB + 23.0              # štítek 60 x 15 mm
if FLUTES:
    fb = bmesh.new()
    for i in range(-23, 24):
        x = i * 4.0
        segs = [(ZB + 5, FZ - 5)]
        if HANDLE and abs(x) <= 56:
            segs = [(PZ + 9, FZ - 5)] if abs(x) <= 34 else [(ZB + 5, PZ - 9), (PZ + 9, FZ - 5)]
        for z0, z1 in segs:
            add_cone(fb, 1.3, 1.3, z1 - z0, (x, -0.3, (z0 + z1) / 2), None, 20)
    boolean(body, new_obj("flutes", fb, c_tmp))
    boolean(body, new_obj("label", cuboid(-30, 30, -0.5, 0.6, LAB_Z0, LAB_Z1), c_tmp))   # mělká kapsa pro štítek / samolepku

# žebro (pod klíčem uvnitř horního okraje stěny, ~50° = bez podpor) a klíč (rybina)
RIB = [(85.0, ZR), (99.5, ZR), (99.5, ZR - 14), (100.0 - WALL, ZR - 14)]
for s in (+1, -1):
    boolean(body, new_obj("rib", prism_xz(mirror(RIB, s), KEY_Y0, KEY_Y1), c_tmp), 'UNION')
    boolean(body, new_obj("key", prism_xz(KEY, KEY_Y0, KEY_Y1, s * XC), c_tmp), 'UNION')

# variabilní vnitřek: dvojice svislých žeber (drážky 2.2 mm) na vnitřních stěnách
#  - příčné přepážky (rovnoběžné s čelem): drážky v bočních stěnách, 7 poloh po 24 mm
#  - podélné přepážky (rovnoběžné s boky): drážky ve vnitřní straně čela a v zadní stěně, 5 poloh po 33 mm
ZF = ZB + BOTTOM
SLOT_GAP = (DIV_T + 0.6) / 2
bm = bmesh.new()
for yc in TR_Y:
    for s in (+1, -1):
        xw = s * (W / 2 - WALL)
        xa, xb = sorted((xw + s * 0.3, xw - s * 3.0))
        cuboid(xa, xb, yc - SLOT_GAP - 2.4, yc - SLOT_GAP, ZF - 0.3, DIV_TOP, bm)
        cuboid(xa, xb, yc + SLOT_GAP, yc + SLOT_GAP + 2.4, ZF - 0.3, DIV_TOP, bm)
for xc in LG_X:
    for ysd, yw in ((+1, FRONT), (-1, D - WALL)):
        ya, yb = sorted((yw - ysd * 0.3, yw + ysd * 3.0))
        cuboid(xc - SLOT_GAP - 2.4, xc - SLOT_GAP, ya, yb, ZF - 0.3, DIV_TOP, bm)
        cuboid(xc + SLOT_GAP, xc + SLOT_GAP + 2.4, ya, yb, ZF - 0.3, DIV_TOP, bm)
boolean(body, new_obj("slots", bm, c_tmp), 'UNION')

# aretace v zavřené poloze: kapsa pro magnet v horní ploše žebra (otevřená nahoru = bez podpor), protikus je ve spodku lišty
bm = bmesh.new()
for sx in (-1, 1):
    add_cone(bm, MAG_D / 2, MAG_D / 2, MAG_H + 0.4, (sx * MAG_X, MAG_Y, ZR - MAG_H / 2 + 0.2), None, 28)
boolean(body, new_obj("magpk", bm, c_tmp))

# ---------- madlo (samostatný díl tvaru U, tiskne se naplocho; 2x šroub M4 x 16 zevnitř šuplíku) ----------
M_HANDLE = mat("Madlo", (0.12, 0.12, 0.13), 0.4)
handle = None
if HANDLE:
    YF, YG = -(HGAP + HBAR), -HGAP                 # čelní (viditelná) plocha příčky / vnitřní plocha příčky
    RO, RI = 6.5, 3.0                              # rádius zaoblení konců příčky / vnitřních rohů
    def arc(cx, cy, r, a0, a1, n=12):
        return [(cx + r * math.cos(math.radians(a0 + (a1 - a0) * i / n)), cy + r * math.sin(math.radians(a0 + (a1 - a0) * i / n))) for i in range(n + 1)]
    xo, xi = HL / 2, HL / 2 - HLEG
    u = ([(-xo, 0)] + arc(-xo + RO, YF + RO, RO, 180, 270) + arc(xo - RO, YF + RO, RO, 270, 360) + [(xo, 0), (xi, 0)]
         + arc(xi - RI, YG + RI, RI, 0, -90, 8) + arc(-xi + RI, YG + RI, RI, -90, -180, 8) + [(-xi, 0)])
    bm = _extrude(bmesh.new(), [(x, y, 0.0) for x, y in u], (0, 0, HH))
    handle = new_obj("Madlo", bm, c_drawer, M_HANDLE)

    def bevel_z(o, zv, width, seg):                # zaoblení horní/spodní hrany přes váhu hran + modifikátor Bevel
        b = bmesh.new(); b.from_mesh(o.data)
        lay = b.edges.layers.float.get("bevel_weight_edge") or b.edges.layers.float.new("bevel_weight_edge")
        for e in b.edges:
            e[lay] = 1.0 if (all(abs(v.co.z - zv) < 1e-6 for v in e.verts) and not all(abs(v.co.y) < 1e-3 for v in e.verts)) else 0.0
        b.to_mesh(o.data); b.free()
        md = o.modifiers.new("bv", 'BEVEL'); md.limit_method, md.width, md.segments, md.affect = 'WEIGHT', width, seg, 'EDGES'
        for x in bpy.context.selected_objects:
            x.select_set(False)
        o.select_set(True); bpy.context.view_layer.objects.active = o
        bpy.ops.object.modifier_apply(modifier=md.name)
    bevel_z(handle, 0.0, 1.5, 3)                   # spodní hrana (u podložky): menší rádius, bez podpor
    bevel_z(handle, HH, 4.0, 8)                    # horní hrana: velký rádius

    fl = bmesh.new()                               # strukturované zdobení: svislé žlábky na čelní straně
    for i in range(-9, 10):
        add_cone(fl, 1.3, 1.3, HH + 4, (i * 4.0, YF - 0.3, HH / 2), None, 20)
    boolean(handle, new_obj("fl", fl, c_tmp))
    pil = bmesh.new()
    for x in (-HX, HX):    # pilotní díry pro samořezný M4 (3.4 mm, hloubka 12)
        add_cone(pil, 1.7, 1.7, 12.0, (x, -6.0 + 0.5, HH / 2), RX, 24)
    boolean(handle, new_obj("pil", pil, c_tmp))
    handle.location = (0, 0, PZ - HH / 2)
    bpy.context.view_layer.update()
    handle.parent = body                           # jede se zásuvkou

rails = {s: make_rail("Lista_" + ("P" if s > 0 else "L"), s) for s in (+1, -1)}
plugs = {s: make_plug("Zarazka_" + ("P" if s > 0 else "L"), s * XC) for s in (+1, -1)}

# ---------- přepážka (tiskne se naplocho: x šířka, y výška, z tloušťka) ----------
YMID = (FRONT + D - WALL) / 2
def make_divider(kind, c=c_drawer):
    """kind 'T' = příčná (194.2 mm, zářezy shora na polohách LG_X), 'L' = podélná (191.6 mm, zářezy zdola na polohách TR_Y).
    Obě tloušťky 1.6 mm, tisknou se naplocho (x délka, y výška, z tloušťka). Zářezy do sebe zapadají (egg-crate)."""
    dh = DIV_TOP - ZF
    L = (W - 2 * WALL - 1.0) if kind == 'T' else (D - WALL - FRONT - 1.0)
    o = new_obj("Predelka_" + kind, cuboid(-L / 2, L / 2, 0.0, dh, 0.0, DIV_T), c, M_DIV)
    nb = bmesh.new()
    if kind == 'T':
        for x in LG_X:
            cuboid(x - 1.1, x + 1.1, dh - NOTCH, dh + 1.0, -1.0, DIV_T + 1.0, nb)
    else:
        for y in TR_Y:
            cuboid(y - YMID - 1.1, y - YMID + 1.1, -1.0, NOTCH, -1.0, DIV_T + 1.0, nb)
    boolean(o, new_obj("nt", nb, c_tmp))
    if kind == 'L':                                                                     # výřez na prsty nahoře uprostřed
        bm = bmesh.new(); add_cone(bm, 10, 10, DIV_T + 2, (0, dh + 2.0, DIV_T / 2), None, 40)
        boolean(o, new_obj("fn", bm, c_tmp))
    return o

def place_divider(o, kind, pos, parent=None):
    """pos = y od čela (příčná) nebo x od osy (podélná); přepážka stojí na dně v drážkách."""
    if kind == 'T':
        o.rotation_euler = (math.pi / 2, 0, 0); o.location = (0, pos + DIV_T / 2, ZF)
    else:
        o.rotation_euler = (math.pi / 2, 0, math.pi / 2); o.location = (pos - DIV_T / 2, YMID, ZF)
    if parent is not None:
        o.parent = parent

# ---------- vložné přihrádky: 5 druhů, velikosti v násobcích modulu BM ----------
def make_bin(kind, nx, ny, h, c=c_drawer):
    """kind: 'plain' krabička, 'low' nízká, 'cut' s výřezem na prsty vpředu, 'scoop' se šikmým čelem, 'grid' dělená (2 x 2 podbuňky na modul).
    Rozměr nx*BM-BG x ny*BM-BG, stěna a dno 1.2 mm, zaoblené rohy; tiskne se na dně, bez podpor."""
    bx, by = nx * BM - BG, ny * BM - BG
    o = new_obj("Bin_" + kind, prism_z(rrect(-bx / 2, bx / 2, -by / 2, by / 2, 2.5), 0.0, h, chamfer_bottom=0.6), c, M_BIN)
    boolean(o, new_obj("bc", prism_z(rrect(-bx / 2 + BT, bx / 2 - BT, -by / 2 + BT, by / 2 - BT, 1.5), BT, h + 1), c_tmp))
    if kind == 'grid':
        sx, sy = 2 * nx, 2 * ny
        ix, iy = bx - 2 * BT, by - 2 * BT
        wx, wy = bmesh.new(), bmesh.new()
        for i in range(1, sx):
            x = -ix / 2 + i * ix / sx
            cuboid(x - 0.4, x + 0.4, -iy / 2 - 0.3, iy / 2 + 0.3, BT - 0.3, h - 4.0, wx)
        for j in range(1, sy):
            y = -iy / 2 + j * iy / sy
            cuboid(-ix / 2 - 0.3, ix / 2 + 0.3, y - 0.4, y + 0.4, BT - 0.3, h - 4.0, wy)
        boolean(o, new_obj("gx", wx, c_tmp), 'UNION')
        boolean(o, new_obj("gy", wy, c_tmp), 'UNION')
    elif kind == 'cut':          # výřez na prsty v přední stěně (polokruh otevřený nahoru)
        bm = bmesh.new(); add_cone(bm, min(14.0, bx / 2 - 6.0), min(14.0, bx / 2 - 6.0), 4.0, (0, -by / 2, h), RX, 40)
        boolean(o, new_obj("ct", bm, c_tmp))
    elif kind == 'scoop':        # přední stěna jen 16 mm, boční stěny šikmo pod 45° nahoru k zadní stěně
        pts = [(-by / 2 - 1, 15.0), (-by / 2 + h - 15.0, h + 1), (-by / 2 - 1, h + 1)]
        boolean(o, new_obj("sc", prism_yz(pts, -bx / 2 - 1, bx / 2 + 1), c_tmp))
    return o

# katalog: (soubor, druh, nx, ny, výška)
BIN_CATALOG = [
    ("krabicka_1x1", 'plain', 1, 1, BHF), ("krabicka_2x1", 'plain', 2, 1, BHF), ("krabicka_2x2", 'plain', 2, 2, BHF),
    ("krabicka_4x1", 'plain', 4, 1, BHF),
    ("nizka_2x1", 'low', 2, 1, BHL), ("nizka_2x2", 'low', 2, 2, BHL), ("nizka_4x1", 'low', 4, 1, BHL),
    ("vyrez_1x1", 'cut', 1, 1, BHF), ("vyrez_2x1", 'cut', 2, 1, BHF), ("vyrez_2x2", 'cut', 2, 2, BHF),
    ("sikma_2x1", 'scoop', 2, 1, BHF), ("sikma_2x2", 'scoop', 2, 2, BHF),
    ("delena_1x1", 'grid', 1, 1, BHF), ("delena_2x1", 'grid', 2, 1, BHF), ("delena_2x2", 'grid', 2, 2, BHF),
]
# startovní sada, která zaplní celý šuplík (16 modulů): (soubor, sloupec, řada)
BIN_STARTER = [("delena_2x2", 0, 0), ("vyrez_2x1", 2, 0), ("sikma_2x1", 2, 1),
               ("krabicka_4x1", 0, 2), ("krabicka_2x1", 0, 3), ("krabicka_2x1", 2, 3)]
def place_bin(o, nx, ny, a, b, parent=None):
    o.location = (BIN_X0 + (a + nx / 2) * BM, BIN_Y0 + (b + ny / 2) * BM, ZF)
    if parent is not None:
        o.parent = parent

# ---------- montážní šablona pro lišty (drží rozteč 180 mm, zarovná lišty s hranou police) ----------
def make_jig(c=c_drawer):
    z0 = ZR - 6.0
    jig = new_obj("Sablona", cuboid(-95.0, 95.0, -5.0, RAIL_Y0 + 40.0, z0, ZR), c, M_JIG)       # příčka
    boolean(jig, new_obj("jt", cuboid(-15.0, 15.0, -5.0, 0.0, z0, 8.0), c_tmp), 'UNION')       # dorazová deska na hranu police
    for s in (+1, -1):
        boolean(jig, new_obj("jk", prism_xz(KEY, RAIL_Y0, RAIL_Y0 + 40.0, s * XC), c_tmp), 'UNION')
    wb = bmesh.new()
    for s in (+1, -1):                                                      # okna pro šroubování uší lišt
        xa, xb = sorted((s * 61.0, s * 77.0))
        cuboid(xa, xb, RAIL_Y0 + 3.0, RAIL_Y0 + 40.0, z0 - 1.0, ZR + 1.0, wb)
    boolean(jig, new_obj("jw", wb, c_tmp))
    return jig

# ---------- police a více zásuvek vedle sebe ----------
SHELF_W = N * P + 40
shelf = new_obj("Police", cuboid(-SHELF_W / 2, SHELF_W / 2, 0.0, SHELF_D, 0.0, SHELF_T), c_shelf, M_WOOD)

mid = N // 2
sets = {mid: {"body": body, "rails": rails, "plugs": plugs, "handle": handle}}
for i in range(N):
    if i == mid:
        continue
    dx = (i - mid) * P
    def cp(o, c, m=None):
        n = bpy.data.objects.new("%s_%d" % (o.name, i), o.data); n.location = (dx + o.location.x, o.location.y, o.location.z)
        c.objects.link(n)
        return n
    b = cp(body, c_drawer); b.data = body.data.copy(); b.data.materials.clear(); b.data.materials.append(M_DRAWERS[i % 3])
    sets[i] = {"body": b, "rails": {s: cp(rails[s], c_rail) for s in rails}, "plugs": {s: cp(plugs[s], c_rail) for s in plugs},
               "handle": cp(handle, c_drawer) if handle else None}
bpy.context.view_layer.update()

# ---------- kontrola: manifold, překryvy, sousedé ----------
def mesh_stats(o):
    bm = bmesh.new(); bm.from_mesh(o.data)
    n = sum(1 for e in bm.edges if not e.is_manifold)
    vol = bm.calc_volume(signed=True); bm.free()
    return n, round(vol / 1000.0, 1)

def overlap(a, b):
    t = bpy.data.objects.new("t", a.data.copy()); t.matrix_world = a.matrix_world.copy(); c_tmp.objects.link(t)
    md = t.modifiers.new("i", 'BOOLEAN'); md.operation, md.object, md.solver = 'INTERSECT', b, 'EXACT'
    bpy.context.view_layer.update()
    me = bpy.data.meshes.new_from_object(t.evaluated_get(bpy.context.evaluated_depsgraph_get()))
    bm = bmesh.new(); bm.from_mesh(me); v = bm.calc_volume(); bm.free()
    bpy.data.objects.remove(t, do_unlink=True); bpy.data.meshes.remove(me)
    return round(abs(v), 3)

report = {"non_manifold_hrany_a_objem_cm3": {o.name: mesh_stats(o) for o in (body, shelf, rails[1], rails[-1], plugs[1])}}
_v = {o.name: mesh_stats(o)[1] for o in (body, rails[1], rails[-1], plugs[1])}
_tot = _v["Supl"] + _v["Lista_P"] + _v["Lista_L"] + 2 * _v["Zarazka_P"]
report["sada_na_1_zasuvku: plny_objem_cm3 / max_hmotnost_g_PETG (bez madla)"] = (round(_tot, 1), round(1.27 * _tot))
chk = {}
for shift in (0.0, TRAVEL - 0.5, TRAVEL / 2):                           # zavřeno / téměř na zarážce / uprostřed
    body.location = (0, -shift, 0); bpy.context.view_layer.update()
    chk["posun_%.0f: supl x lista/zarazka" % shift] = sum(overlap(body, r) for r in rails.values()) + sum(overlap(body, p) for p in plugs.values())
body.location = (0, -(TRAVEL + 1.0), 0); bpy.context.view_layer.update()
chk["KONTROLA CHECKERU: klic narazi na zarazku (ma byt > 0)"] = sum(overlap(body, p) for p in plugs.values())
body.location = (0, 0, 0); bpy.context.view_layer.update()
chk["lista x zarazka"] = sum(overlap(rails[s], plugs[s]) for s in (+1, -1))
chk["lista x police (dosedaji)"] = sum(overlap(shelf, r) for r in rails.values())
nb = 0
for i in sets:                                                          # sousední zásuvky se nesmí dotýkat
    for j in sets:
        if i < j:
            a, b = sets[i], sets[j]
            nb += overlap(a["body"], b["body"])
            nb += sum(overlap(a["body"], r) for r in b["rails"].values()) + sum(overlap(b["body"], r) for r in a["rails"].values())
            nb += sum(overlap(ra, rb) for ra in a["rails"].values() for rb in b["rails"].values())
if handle:
    for i in sets:
        for j in sets:
            h, o = sets[i]["handle"], sets[j]
            nb += (0 if i == j else overlap(h, o["body"])) + sum(overlap(h, r) for r in o["rails"].values()) + (0 if i == j else overlap(h, o["handle"]))
    chk["madlo x telo"] = overlap(handle, body)
    chk["madlo x police"] = overlap(handle, shelf)
chk["sousedni zasuvky/listy/madla (ma byt 0)"] = nb
chk["zasuvka x police (nesmi se dotykat)"] = overlap(body, shelf)

# přepážky v drážkách a šablona na lištách (vpředu i vzadu)
dT = [(y, make_divider('T', c_tmp)) for y in TR_Y]
dL = [(x, make_divider('L', c_tmp)) for x in LG_X]
for y, d in dT:
    place_divider(d, 'T', y)
for x, d in dL:
    place_divider(d, 'L', x)
bpy.context.view_layer.update()
chk["prepazky (7 pricnych + 5 podelnych) x telo zasuvky"] = sum(overlap(d, body) for _, d in dT + dL)
chk["pricne x podelne (zarezy, vsech 35 kriziku)"] = sum(overlap(t, l) for _, t in dT for _, l in dL)
divs = [d for _, d in dT + dL]
cat = {n: (k, nx, ny, h) for n, k, nx, ny, h in BIN_CATALOG}
starter = []
for n, a, b in BIN_STARTER:
    k, nx, ny, h = cat[n]
    o = make_bin(k, nx, ny, h, c_tmp); place_bin(o, nx, ny, a, b); starter.append(o)
bpy.context.view_layer.update()
chk["startovni prihradky (6 ks) x telo zasuvky"] = sum(overlap(o, body) for o in starter)
chk["startovni prihradky mezi sebou (15 dvojic)"] = sum(overlap(starter[i], starter[j]) for i in range(len(starter)) for j in range(i + 1, len(starter)))
divs += starter
jig = make_jig(c_tmp)
bpy.context.view_layer.update()
jig_c = {"sablona vpredu x listy": sum(overlap(jig, r) for r in rails.values()),
         "sablona x police": overlap(jig, shelf)}
jig.location = (0, GROOVE_END - 0.5 - (RAIL_Y0 + 40.0), 0)               # zasunutá až dozadu (šroubování zadních uší)
bpy.context.view_layer.update()
jig_c["sablona vzadu x listy"] = sum(overlap(jig, r) for r in rails.values())
chk.update(jig_c)
for d in divs:
    bpy.data.objects.remove(d, do_unlink=True)
bpy.data.objects.remove(jig, do_unlink=True)
report["prekryvy_mm3 (vse ma byt 0, krome kontroly checkeru)"] = chk

# ukázkové dělení v prostřední zásuvce (v .blend a na náhledech): 2 podélné + 2 příčné přepážky
sample = []
for kind, pos in (('L', -33.0), ('L', 33.0), ('T', 77.0), ('T', 125.0)):
    o = make_divider(kind, c_drawer); place_divider(o, kind, pos, parent=body); sample.append(o)
for n, a, b in BIN_STARTER:                       # ukázka přihrádek v pravé zásuvce
    k, nx, ny, h = cat[n]
    o = make_bin(k, nx, ny, h, c_drawer); place_bin(o, nx, ny, a, b, parent=sets[N - 1]["body"]); sample.append(o)
bpy.context.view_layer.update()

# ---------- animace: otevřít prostřední zásuvku ----------
sc.frame_start, sc.frame_end = 1, 60
body.keyframe_insert("location", frame=1)
body.location = (0, -TRAVEL, 0); body.keyframe_insert("location", frame=60)
body.location = (0, 0, 0); sc.frame_set(1)

# ---------- svět, světla, kamery ----------
w = bpy.data.worlds.new("w"); w.use_nodes = True; sc.world = w
w.node_tree.nodes["Background"].inputs[0].default_value = (0.2, 0.22, 0.25, 1)
def light(name, loc, rot, e, size=500):
    d = bpy.data.lights.new(name, 'AREA'); d.energy, d.size = e, size
    o = bpy.data.objects.new(name, d); o.location, o.rotation_euler = loc, rot; sc.collection.objects.link(o); return o
light("key", (350, -700, -120), (1.35, 0, 0.35), 1.2e6, 600)
light("fill", (-450, -450, -300), (1.1, 0, -0.6), 4.0e5, 600)
top = light("top", (0, 0, 700), (0, 0, 0), 9.0e5, 700); top.hide_render = True

tgt = bpy.data.objects.new("cil", None); tgt.location = (0, 40, -42); sc.collection.objects.link(tgt)
def mkcam(name, loc):
    c = bpy.data.objects.new(name, bpy.data.cameras.new(name)); c.data.lens = 40
    c.location = loc; sc.collection.objects.link(c)
    t = c.constraints.new('TRACK_TO'); t.target = tgt; t.track_axis = 'TRACK_NEGATIVE_Z'; t.up_axis = 'UP_Y'
    return c
cam = mkcam("Kamera", (330, -760, -190)); sc.camera = cam
cam_top = mkcam("Kamera_shora", (260, -560, 520))
sc.render.engine = 'CYCLES'; sc.cycles.samples = 48; sc.cycles.use_denoising = True
sc.render.resolution_x, sc.render.resolution_y = 1600, 900

# ---------- STL v tiskové orientaci + kontrola převisů ----------
for f in glob.glob(os.path.join(OUT, "stl", "*.stl")):
    os.remove(f)
os.makedirs(os.path.join(OUT, "stl"), exist_ok=True)

def export_print(o, fname, rot=None):
    me = o.data.copy(); me.transform(o.matrix_world)
    if rot is not None:
        me.transform(rot)
    zs = [v.co.z for v in me.vertices]
    me.transform(Matrix.Translation((0, 0, -min(zs))))
    xs = [v.co.x for v in me.vertices]; ys = [v.co.y for v in me.vertices]; zs = [v.co.z for v in me.vertices]
    bm = bmesh.new(); bm.from_mesh(me); bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    ov = sum(f.calc_area() for f in bm.faces if f.normal.z < -0.72 and f.calc_center_median().z > 0.05); bm.free()
    t = bpy.data.objects.new(fname, me); c_tmp.objects.link(t)
    for x in bpy.context.selected_objects:
        x.select_set(False)
    t.select_set(True); bpy.context.view_layer.objects.active = t
    bpy.ops.wm.stl_export(filepath=os.path.join(OUT, "stl", fname + ".stl"), export_selected_objects=True)
    bpy.data.objects.remove(t, do_unlink=True)
    return {"rozmer_mm": (round(max(xs) - min(xs), 1), round(max(ys) - min(ys), 1), round(max(zs), 1)),
            "plocha_previsu_nad_45_mm2": round(ov, 1)}

flip = Matrix.Rotation(math.pi, 4, 'Y')            # lišta: montážní plochou na podložku
stand = Matrix.Rotation(math.pi / 2, 4, 'X')       # zarážka: průřezem na podložku
info = {
    "supl.stl (1x na zasuvku)": export_print(body, "supl"),
    "lista_P.stl (1x na zasuvku)": export_print(rails[1], "lista_P", flip),
    "lista_L.stl (1x na zasuvku)": export_print(rails[-1], "lista_L", flip),
    "zarazka_2x.stl (2x na zasuvku)": export_print(plugs[1], "zarazka_2x", stand),
}
if handle:
    info["madlo_1x.stl (1x na zasuvku)"] = export_print(handle, "madlo_1x")
dvT, dvL = make_divider('T', c_tmp), make_divider('L', c_tmp)
info["predelka_prycna.stl (volitelne, 0-7 ks)"] = export_print(dvT, "predelka_prycna")
info["predelka_podelna.stl (volitelne, 0-5 ks)"] = export_print(dvL, "predelka_podelna")
for n, k, nx, ny, h in BIN_CATALOG:
    bo = make_bin(k, nx, ny, h, c_tmp)
    info["%s.stl" % n] = export_print(bo, n)
    bpy.data.objects.remove(bo, do_unlink=True)
jig = make_jig(c_tmp)
info["sablona_listy.stl (1x, staci na vsechny zasuvky)"] = export_print(jig, "sablona_listy")
for cl, nd in ((0.3, 1), (0.4, 2), (0.5, 3)):          # zkouška vůle: 1, 2, 3 důlky = CL 0.3, 0.4, 0.5
    tr = make_rail("t_%d" % nd, 1, y_len=30.0, holes=False, stop_hole=False, groove_end=RAIL_Y0 + 31.0, c=c_tmp, xoff=0.0, cl=cl, dimples=nd)
    info["test_vule_%.2f.stl" % cl] = export_print(tr, "test_vule_%.2f" % cl, flip)
    bpy.data.objects.remove(tr, do_unlink=True)
tp = new_obj("t2", prism_xz([(-13, ZR - 4), (13, ZR - 4), (13, ZR), (-13, ZR)], 0, 30), c_tmp, M_RAIL)
boolean(tp, new_obj("tk", prism_xz(KEY, 0, 30), c_tmp), 'UNION')
info["test_klic.stl"] = export_print(tp, "test_klic")
for o in (tp, dvT, dvL, jig):
    bpy.data.objects.remove(o, do_unlink=True)
bpy.data.collections.remove(c_tmp)
report["tiskove_dily"] = info
report["vysun_mm"] = TRAVEL
print("REPORT", report)

# ---------- uložení + náhledy ----------
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT, "supl_police.blend"))
def render(name):
    sc.render.filepath = os.path.join(OUT, name)
    try:
        bpy.ops.render.render(write_still=True)
    except RuntimeError:
        sc.render.filepath = os.path.join(OUT, name.replace(".png", "_v2.png"))      # soubor je zamčený (otevřený v prohlížeči)
        bpy.ops.render.render(write_still=True)
for frame, name in ((1, "nahled_zavrena.png"), (60, "nahled_otevrena.png")):
    sc.frame_set(frame)
    render(name)
shelf.hide_render = True; top.hide_render = False; sc.camera = cam_top
render("nahled_shora_otevrena.png")

# rozložený pohled (jen pro náhled, do .blend se neukládá)
sc.frame_set(1); shelf.hide_render = False
body.animation_data_clear()
for s in (+1, -1):
    rails[s].location.z -= 55; plugs[s].location = (0, -45, -55)
body.location = (0, -30, -120)
if handle:
    handle.location.y -= 55
top.hide_render = True; sc.camera = cam
cam.location = (300, -620, -230); tgt.location = (0, -20, -70)
render("nahled_rozlozeny.png")
print("HOTOVO")
