#!/usr/bin/env python
"""P-002 / BRIM-011 — pocket pill box with a rotating vault-door lid.

Three parts, trimesh + manifold3d CSG, all mm, Z up, printed as oriented.
  base : Ø60 × 16 cup, six 60° sectors (five open + ONE SOLID parking
         sector), central boss with a pin hole, six detent BUMPS (r 1.0
         domes) on the top of the 2.4 mm wall at 30° + k·60°.
  lid  : Ø60 vault-door disc, one 54° window, twelve gear teeth, raised
         rings + ribs (grip and looks), six detent DIMPLES in its flat
         underside that seat on the base bumps when the window sits over a
         sector. Flat underside → prints face-up with no supports.
  pin  : slotted post with a hub head, exported HEAD-DOWN (no supports);
         pushes through the lid into the boss; pull to pop the lid off.
Also writes lid_control (NO dimples) — the negative control for the
detent check: the base bumps must interfere with it and not with lid.
Mechanism prior art: printables.com/model/1582116 (AP Designs, CC0) —
rotating 7-day organiser on a pin. This geometry is written from scratch.
"""
import sys, json, numpy as np, trimesh
from trimesh.creation import box, cylinder, icosphere

# ---------------- PARAMS ----------------
OD        = 60.0    # outer diameter
H         = 16.0    # base height
WALL      = 2.4     # outer wall (carries the dimples)
FLOOR     = 1.5
DIV_T     = 1.2     # divider thickness
BOSS_D    = 9.0     # central boss
PIN_HOLE  = 3.5     # boss hole (pin shaft nominal 3.5: slot gives compliance)
PIN_DEPTH = 11.0
SOLID_SECTOR = 0    # index k: sector between k·60° and (k+1)·60° is solid
BUMP_R    = 1.0     # base bump radius (dome on the wall top, protrudes 1.0)
DIMPLE_R  = 1.2     # lid dimple sphere radius (cut into the flat underside)
DIMPLE_ZC = -0.2    # sphere centre this far BELOW the lid underside → depth 1.0
DET_ANG   = 30.0    # dimples/bumps at DET_ANG + k·60
LID_T     = 2.5
WIN_A0, WIN_A1 = 3.0, 57.0     # window angular span (inside sector 0)
WIN_R0, WIN_R1 = 6.5, 27.0     # window radial span
TEETH, TOOTH_W, TOOTH_H = 12, 4.0, 3.0
RING_H    = 1.0
LID_HOLE  = 3.8
HEAD_D, HEAD_T = 9.0, 2.0
PIN_D     = 3.5
SLOT_W, SLOT_L = 0.8, 8.0
# ----------------------------------------
R = OD/2; RI = R - WALL; SEG = 128

def cz(r, z0, z1, x=0, y=0, seg=SEG):
    c = cylinder(radius=r, height=z1-z0, sections=seg); c.apply_translation([x, y, (z0+z1)/2]); return c
def bx(x0,x1,y0,y1,z0,z1):
    b = box(extents=[x1-x0, y1-y0, z1-z0]); b.apply_translation([(x0+x1)/2,(y0+y1)/2,(z0+z1)/2]); return b
def rotz(m, deg):
    m = m.copy(); m.apply_transform(trimesh.transformations.rotation_matrix(np.radians(deg), [0,0,1])); return m
def sector(r0, r1, a0, a1, z0, z1, n=None):
    """Annular sector as primitives only: ring ∩ half-space(a0) ∩ half-space(a1); a1-a0 must be < 180."""
    ring = cz(r1, z0, z1) if r0 <= 0 else diff(cz(r1, z0, z1), [cz(r0, z0-1, z1+1)])
    big = 4*R
    h0 = rotz(bx(-big, big, 0, big, z0-1, z1+1), a0)        # y>=0 rotated to a0  → left of a0 ray
    h1 = rotz(bx(-big, big, -big, 0, z0-1, z1+1), a1)       # y<=0 rotated to a1  → right of a1 ray
    m = trimesh.boolean.intersection([ring, h0, h1], engine="manifold")
    return m
def sph(r, x, y, z):
    s = icosphere(subdivisions=3, radius=r); s.apply_translation([x,y,z]); return s
def union(ms): return trimesh.boolean.union(ms, engine="manifold")
def diff(a, ms): return trimesh.boolean.difference([a]+ms, engine="manifold")
def inter(a, b): return trimesh.boolean.intersection([a,b], engine="manifold")

def make_base():
    cup = diff(cz(R, 0, H), [cz(RI, FLOOR, H+1)])
    divs = [rotz(bx(0, RI+0.5, -DIV_T/2, DIV_T/2, 0, H), k*60) for k in range(6)]
    solid = sector(0, RI+0.5, SOLID_SECTOR*60, SOLID_SECTOR*60+60, 0, H)
    boss = cz(BOSS_D/2, 0, H)
    rc = R - WALL/2
    bumps = [sph(BUMP_R, rc*np.cos(np.radians(DET_ANG+k*60)), rc*np.sin(np.radians(DET_ANG+k*60)), H) for k in range(6)]
    b = union([cup, boss, solid] + divs + bumps)
    return diff(b, [cz(PIN_HOLE/2, H-PIN_DEPTH, H+1, seg=48)])

def make_lid(dimples=True):
    disc = cz(R, 0, LID_T)
    teeth = [rotz(bx(R-1.0, R+TOOTH_H, -TOOTH_W/2, TOOTH_W/2, 0, LID_T), k*360/TEETH) for k in range(TEETH)]
    rings = [diff(cz(R-2.0, LID_T, LID_T+RING_H), [cz(R-4.0, LID_T-1, LID_T+2)]),
             diff(cz(10.0, LID_T, LID_T+RING_H), [cz(8.0, LID_T-1, LID_T+2)])]
    ribs = [rotz(bx(10.0, R-4.0, -0.6, 0.6, LID_T, LID_T+0.8), DET_ANG + k*60) for k in range(6) if k != 0]
    lid = union([disc] + teeth + rings + ribs)
    win = sector(WIN_R0, WIN_R1, WIN_A0, WIN_A1, -5, LID_T+5)
    cuts = [win, cz(LID_HOLE/2, -5, LID_T+5, seg=48)]
    if dimples:
        rc = R - WALL/2
        cuts += [sph(DIMPLE_R, rc*np.cos(np.radians(DET_ANG+k*60)), rc*np.sin(np.radians(DET_ANG+k*60)), DIMPLE_ZC) for k in range(6)]
    return diff(lid, cuts)

def make_pin():
    # exported HEAD-DOWN: head z 0..HEAD_T on the bed, shaft up, slot at the free end
    L = LID_T + RING_H + PIN_DEPTH - 0.5
    head = cz(HEAD_D/2, 0, HEAD_T, seg=64)
    shaft = cz(PIN_D/2, HEAD_T, HEAD_T+L, seg=48)
    pin = union([shaft, head])
    return diff(pin, [bx(-SLOT_W/2, SLOT_W/2, -PIN_D, PIN_D, HEAD_T+L-SLOT_L, HEAD_T+L+1)])

def report(name, m):
    return dict(part=name, watertight=bool(m.is_watertight), volume_mm3=round(float(m.volume),1),
                bbox=[round(float(v),2) for v in m.extents], faces=int(len(m.faces)))

if __name__ == "__main__":
    out = sys.argv[1] if len(sys.argv) > 1 else "."
    base = make_base(); lid = make_lid(True); ctrl = make_lid(False); pin = make_pin()
    parts = dict(p002_base=base, p002_lid=lid, p002_pin=pin, p002_lid_control=ctrl)
    rep = [report(k, v) for k, v in parts.items()]
    # --- checks ---
    # 1. window sits inside sector 1..5 region? At rotation 0 the window (3°–57°) must lie over an OPEN sector, i.e. not the solid one.
    #    Define assembly: lid at z=H. Sample window centre ray and probe base solid below.
    a = np.radians((WIN_A0+WIN_A1)/2); rmid = (WIN_R0+WIN_R1)/2
    inside = []
    for k in range(6):
        x, y = rmid*np.cos(a+np.radians(k*60)), rmid*np.sin(a+np.radians(k*60))
        pb = bx(x-1, x+1, y-1, y+1, H-5, H-3)
        ib = inter(pb, base); inside.append(bool(len(ib.faces) and ib.volume > 0.5))
    inside = np.array(inside)
    # 2. detent check with negative control: bumps (lid at z=H) vs base and vs control
    lidA = lid.copy(); lidA.apply_translation([0,0,H]); ctrlA = ctrl.copy(); ctrlA.apply_translation([0,0,H])
    bumps_only = inter(base, cz(R+2, H+0.001, H+BUMP_R+1))     # the only base material above the wall top = the six domes
    i_base = inter(bumps_only, lidA); i_ctrl = inter(bumps_only, ctrlA)
    v_base = float(i_base.volume) if len(i_base.faces) else 0.0
    v_ctrl = float(i_ctrl.volume) if len(i_ctrl.faces) else 0.0
    # 3. lid clears the boss/dividers/wall everywhere else: base material at or below the wall top vs the lid must be 0
    base_body = inter(base, cz(R+2, -1, H))
    i_body = inter(base_body, lidA); v_body = float(i_body.volume) if len(i_body.faces) else 0.0
    # 4. sector capacity: interior of one open sector
    cav = diff(sector(BOSS_D/2+DIV_T, RI, 60+DIV_T, 120-DIV_T, FLOOR, H), [base]); cap = float(cav.volume)/1000.0
    # 5. pin fits: shaft vs hole clearance, lid hole
    checks = dict(
        window_over_solid_sector_at_k=[int(k) for k in range(6) if inside[k]],
        window_positions_open=int((~inside).sum()),
        detent_interference_mm3=round(v_base,3), control_interference_mm3=round(v_ctrl,3),
        detent_PASS=bool(v_base < 0.05 and v_ctrl > 1.0),
        lid_body_interference_mm3=round(v_body,3), lid_body_PASS=bool(v_body < 0.05),
        sector_capacity_ml=round(cap,2), pin_hole_clearance_mm=round(PIN_HOLE-PIN_D,2), lid_hole_clearance_mm=round(LID_HOLE-PIN_D,2),
        assembled_height_mm=round(H+LID_T+RING_H+HEAD_T,1), footprint_mm=round(OD+2*TOOTH_H,1))
    for k, v in parts.items(): v.export(f"{out}/{k}.stl")
    print(json.dumps(dict(parts=rep, checks=checks), indent=1))
