#!/usr/bin/env python
"""C-02 — a retained clamshell case for the ChocolateLoverRaj GSC / SuzyQ
debug board (v4.1.0).  trimesh + manifold3d CSG, all mm.  **v1 (wake 126).**

Why this shape: the prior-art shell (Printables 1402518, "C3D") failed F1
because its rear cap sits IN the load path — every USB-C insertion pushes the
board into a friction-fit cap, and the cap left on cycle 1.  Here the board is
sandwiched between two identical halves.  Insertion load (board pushed
rearward) goes from the receptacle's front face into a REAR WALL that is part
of the halves; removal load (board pulled forward) goes from the PCB front edge
into the step at the back of the nose.  Both are compression into the walls;
the joint between the halves carries none of it.  The halves are keyed by two
pegs (+Y side) into two holes (-Y side): flip one half 180° about the long
axis and it mates with itself, so ONE STL, print 2.

v1 changes (other-011's calipers + the C3D STL read properly, wake 126):
- PCB 19.25 / plug 13.65 / receptacle 11.66 wide with NO overhang.
- The nose copies the C3D geometry that demonstrably seated this board: a
  short front LIP whose opening passes only the plug shell (C3D 8.6 x 2.6 →
  8.8 x 2.8 here), then a full-height channel 8.8 wide (C3D 8.8 x 4.8).
  v0's 3.2-tall nose channel was a guess; C3D's nose is 4.8 tall for a reason.
- The rear collar is gone.  The receptacle opens flush with the PCB rear
  edge, so anything thick behind it keeps a cable's overmold from reaching
  the receptacle.  Rear wall = 1.8 (C3D's cap plate) with a window that
  copies C3D's cap window in width (9.0) and is 3.0 tall — the receptacle
  shell (3.22 overall) cannot enter it, a cable's plug shell (2.6) can.
  That is exactly the face the C3D cap bore on for twenty cycles.
- Rear peg/hole sites move to two outboard lugs at the rear corners (the
  1.8 wall cannot hold a Ø1.45 hole).  Envelope stays 14.4 wide except the
  last 3 mm, which are 18.4 across the lugs — the Chromebook end is unchanged.

Axes: X along the board (plug end = -X), Y across, Z up; the half is
generated floor-down, split plane at Z = SPLIT.  PCB front edge at X = 0.
Datum (Julio's calipers, trusted channel 2026-08-24): overall 32.86; PCB
19.25 x 12.07 x 1.24; plug protrudes 13.65 past the PCB edge; 3.52 overall at
the plug end; receptacle 11.66 wide, 3.22 overall, no overhang.

Usage: c02_gen.py [out.stl] [--peg D] [--hole D]   (defaults 1.25 / 1.45)
"""
import sys, numpy as np, trimesh
from trimesh.creation import box, cylinder

# ---------------- PARAMS ----------------
PCB_LEN        = 19.25   # bare PCB front edge -> rear edge   (other-011 f, measured)
PLUG_PROTRUDE  = 13.65   # plug tip -> PCB front edge          (other-011 g, measured)
RECEPT_W       = 11.66   # receptacle shell width              (other-011 h, measured)
RECEPT_OVERHANG= 0.0     # past the PCB rear edge              (other-011: none)
PCB_W          = 12.07   # measured
PCB_T          = 1.24    # measured
BOARD_T_MAX    = 3.52    # measured, overall at the plug end
RECEPT_T       = 3.22    # measured, overall at the receptacle end
CAV_W          = 12.40   # cavity width  (= C3D's, "drops in easy")
CAV_H          = 4.80    # cavity height (= C3D's)
AX_CLEAR       = 0.15    # axial clearance each end (board floats 0.30 total)
WALL_SIDE      = 1.0
FLOOR          = 0.8
EXPOSED        = 7.0     # plug left outside the lip (USB-C mates at 6.65; C3D left ~6.8)
LIP            = 1.2     # front lip thickness
LIP_W, LIP_H   = 8.8, 2.8   # lip opening: only the plug shell passes (C3D 8.6 x 2.6 seated)
NOSE_CH_W      = 8.8     # nose channel behind the lip, full cavity height (C3D 8.8 x 4.8)
REAR_WALL      = 1.8     # = C3D's cap plate thickness
WIN_W, WIN_H   = 9.0, 3.0   # rear window: C3D 9.0 x 3.2; 3.0 so the 3.22 shell cannot enter
BOSS_L, BOSS_W = 3.0, 2.0   # rear lugs carrying the rear peg/hole
PEG_D, HOLE_D, PEG_H, HOLE_DEPTH = 1.25, 1.45, 1.6, 1.9
# ----------------------------------------
SPLIT  = FLOOR + CAV_H/2                     # 3.2
NOSE   = PLUG_PROTRUDE - EXPOSED             # 6.65 ahead of the PCB edge
X0     = -AX_CLEAR - NOSE                    # case front
X1     = PCB_LEN + AX_CLEAR + REAR_WALL      # case rear
W      = CAV_W + 2*WALL_SIDE                 # 14.4
SEG = 48

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 cyl_z(r, z0, z1, cx, cy):
    c = cylinder(radius=r, height=z1-z0, sections=SEG); c.apply_translation([cx, cy, (z0+z1)/2]); return c
def union(ms): return trimesh.boolean.union(ms, engine="manifold")
def diff(a, ms): return trimesh.boolean.difference([a]+ms, engine="manifold")

def half(peg_d=PEG_D, hole_d=HOLE_D):
    body = union([
        bx(X0, X1, -W/2, W/2, 0, SPLIT),
        bx(X1-BOSS_L, X1,  W/2, W/2+BOSS_W, 0, SPLIT),
        bx(X1-BOSS_L, X1, -W/2-BOSS_W, -W/2, 0, SPLIT),
    ])
    cuts = [
        bx(-AX_CLEAR, PCB_LEN+AX_CLEAR, -CAV_W/2, CAV_W/2, FLOOR, SPLIT+1),            # board cavity
        bx(X0+LIP, 0.001, -NOSE_CH_W/2, NOSE_CH_W/2, FLOOR, SPLIT+1),                  # nose channel, full height
        bx(X0-1, X0+LIP+0.001, -LIP_W/2, LIP_W/2, SPLIT-LIP_H/2, SPLIT+1),             # lip opening (plug shell only)
        bx(PCB_LEN+AX_CLEAR-0.001, X1+1, -WIN_W/2, WIN_W/2, SPLIT-WIN_H/2, SPLIT+1),   # rear window (cable plug)
    ]
    xn = (X0 + 0)/2;            yn = (NOSE_CH_W/2 + W/2)/2        # nose side solids
    xc = X1 - BOSS_L/2;         yc = W/2 + 0.7                     # rear lugs
    sites = [(xn, yn), (xc, yc)]
    cuts += [cyl_z(hole_d/2, SPLIT-HOLE_DEPTH, SPLIT+1, x, -y) for x,y in sites]
    h = diff(body, cuts)
    pegs = [cyl_z(peg_d/2, SPLIT-0.01, SPLIT+PEG_H, x, +y) for x,y in sites]
    return union([h]+pegs), sites

def flipped(m):
    f = m.copy()
    f.apply_transform(trimesh.transformations.rotation_matrix(np.pi, [1,0,0]))
    f.apply_translation([0,0,2*SPLIT])
    return f

def ivol(a, b):
    return trimesh.boolean.intersection([a, b], engine="manifold").volume

if __name__ == "__main__":
    args = sys.argv[1:]
    peg_d, hole_d = PEG_D, HOLE_D
    if "--peg" in args:  peg_d  = float(args[args.index("--peg")+1])
    if "--hole" in args: hole_d = float(args[args.index("--hole")+1])
    pos = [a for i,a in enumerate(args) if not a.startswith("--") and (i==0 or not args[i-1].startswith("--"))]
    out = pos[0] if pos else "make/c02-gsc-case/c02_half.stl"

    h, sites = half(peg_d, hole_d)
    assert h.is_watertight, "half not watertight"
    print(f"half: extents {np.round(h.extents,2)}  volume {h.volume:.1f} mm3  nose {NOSE:.2f}  "
          f"case {X1-X0:.2f} x {W:.2f} (+lugs {W+2*BOSS_W:.1f}) x {2*SPLIT:.2f}  peg {peg_d} hole {hole_d}")
    print(f"sites (x,y): {[(round(x,2), round(y,2)) for x,y in sites]}")
    # wall around each hole: distance from hole edge to nearest solid boundary must be >= 0.45
    print(f"nose hole walls: to channel {round((sites[0][1]-hole_d/2)-NOSE_CH_W/2,3)}  to outer {round(W/2-(sites[0][1]+hole_d/2),3)}")
    print(f"lug  hole walls: to cavity  {round((sites[1][1]-hole_d/2)-CAV_W/2,3)}  to lug edge {round(W/2+BOSS_W-(sites[1][1]+hole_d/2),3)}  "
          f"to rear face {round(X1-(sites[1][0]+hole_d/2),3)}  to lug front {round((sites[1][0]-hole_d/2)-(X1-BOSS_L),3)}")
    # mating check: flipped copy must not intersect (pegs in holes with clearance)
    f = flipped(h)
    inter = ivol(h, f)
    print(f"mate overlap volume: {inter:.4f} mm3 (expect ~0 at clearance; >0 only for an interference variant)")
    if hole_d > peg_d: assert inter < 0.05, "halves collide when mated"
    # negative control: shifted 0.6 mm in Y the pegs must collide with solid
    g = f.copy(); g.apply_translation([0, 0.6, 0])
    inter2 = ivol(h, g)
    print(f"negative control (0.6 mm Y shift) overlap: {inter2:.3f} mm3 (expect > 0)")
    assert inter2 > 0.05, "negative control failed: mating check cannot fail"
    # assembly
    asm = union([h, f])
    print(f"assembly watertight {asm.is_watertight}, volume {asm.volume:.1f}, extents {np.round(asm.extents,2)}")
    # things that must FIT (0 overlap with the closed assembly)
    board = bx(0, PCB_LEN, -PCB_W/2, PCB_W/2, SPLIT-BOARD_T_MAX/2, SPLIT+BOARD_T_MAX/2)
    plug  = bx(X0-2, 0, -8.4/2, 8.4/2, SPLIT-2.6/2, SPLIT+2.6/2)                      # plug shell through lip + nose
    c3dn  = bx(X0+LIP+0.05, -0.05, -8.6/2, 8.6/2, SPLIT-4.6/2, SPLIT+4.6/2)           # whatever passed C3D's 8.8x4.8 nose
    rec   = bx(PCB_LEN-6, PCB_LEN+RECEPT_OVERHANG, -RECEPT_W/2, RECEPT_W/2, SPLIT-RECEPT_T/2, SPLIT+RECEPT_T/2)
    cable = bx(PCB_LEN, X1+3, -8.4/2, 8.4/2, SPLIT-2.6/2, SPLIT+2.6/2)                # a cable's plug shell through the window
    for name, part in [("board", board), ("plug shell", plug), ("C3D nose envelope", c3dn), ("receptacle", rec), ("cable plug via window", cable)]:
        v = ivol(asm, part)
        print(f"  FIT  {name:24s} overlap {v:.4f} mm3 (expect 0)")
        assert v < 0.01, f"{name} collides with case"
    # things that must be STOPPED (positive overlap = the wall is there)
    rec_thru = bx(PCB_LEN+AX_CLEAR, X1, -RECEPT_W/2, RECEPT_W/2, SPLIT-RECEPT_T/2, SPLIT+RECEPT_T/2)
    pcb_fwd  = bx(-AX_CLEAR-1.0, -AX_CLEAR, -PCB_W/2, PCB_W/2, SPLIT-PCB_T/2, SPLIT+PCB_T/2)
    for name, part in [("receptacle into rear wall", rec_thru), ("PCB edge into nose step", pcb_fwd)]:
        v = ivol(asm, part)
        print(f"  STOP {name:24s} overlap {v:.3f} mm3 (expect > 0)")
        assert v > 0.5, f"{name}: no wall where the load must go"
    h.export(out); print("wrote", out)
