"""Wibwob Blimp v0 gondola tray — parametric CadQuery model.
Run:  uv run --with cadquery python tray.py
Out:  tray.stl · motor_clip.stl · tray_top.svg · tray_side.svg · a BOM of what goes where (stdout)

Design intent (see ../index.html § The gondola):
- one open-top tray, printed flat, no supports; floor perforated for the lift fan's intake
- board A stands against the front wall, camera looking forward through a window
- board B lies flat at the rear (x = -40), camera looking down through a floor hole; battery B behind it on the rear pad
- one 450 mAh cell at each end, on solid floor pads, velcroed
- two Ø2 mm carbon rods (or Ø3 bamboo skewers) pass through both side walls: the outriggers
  for M1/M2; motor clips slide onto the rod ends. Rod holes are Ø3.3 to take either.
- the lift motor M3 uses the same clip, glued under the floor at the centre of the perforated field
- four hanging ears with Ø2 holes at the corners for the yoke loop and the guy lines
All dimensions mm. Target mass in PLA ~12 g."""
import cadquery as cq, os
PERF = os.environ.get('TRAY_PERF','1') == '1'
EXPORT = os.environ.get('TRAY_EXPORT','1') == '1'

# ---- parameters -------------------------------------------------------------
L, W, H = 140.0, 80.0, 10.0        # outer length (x, forward = +x), width (y), wall height (z)
WALL, FLOOR = 1.0, 0.8
PAD_LEN, PAD_W = 20.0, 62.0        # solid floor pads at each end (battery 59 × 17 lies across)
HOLE_D, PITCH = 5.0, 7.0           # floor perforation, hex grid
CAM_B_D = 10.0                     # board B down-camera hole
CAM_A_D = 9.0                      # board A forward window in the front wall
CAM_A_Z = 6.0                      # window centre height above floor top
ROD_D = 3.3                        # outrigger rod holes (2 mm carbon or 3 mm skewer)
ROD_X = (-9.0, 9.0)                # two rods, either side of centre
ROD_Z = FLOOR + 5.0
EAR = 4.0                          # hanging ear size
MOTOR_D = 8.6                      # 8520 motor press-fit
CLIP_H = 9.0

# ---- tray ---------------------------------------------------------------------
outer = cq.Workplane("XY").box(L, W, H, centered=(True, True, False))
inner = (cq.Workplane("XY").workplane(offset=FLOOR)
         .box(L - 2*WALL, W - 2*WALL, H, centered=(True, True, False)))
tray = outer.cut(inner)

# floor perforation: hex grid of holes, skipping the two end pads and the camera/boss zones
pts = []
row = 0
y = -W/2 + WALL + HOLE_D
while y < W/2 - WALL - HOLE_D/2:
    x0 = -L/2 + WALL + HOLE_D + (PITCH/2 if row % 2 else 0)
    x = x0
    while x < L/2 - WALL - HOLE_D/2:
        in_pad = abs(x) > L/2 - WALL - PAD_LEN
        near_camB = (x + 40.0)**2 + y**2 < (CAM_B_D/2 + 3.0)**2
        near_boss = x**2 + y**2 < (MOTOR_D/2 + 4.0)**2
        if not (in_pad or near_camB or near_boss):
            pts.append((x, y))
        x += PITCH
    y += PITCH * 0.866
    row += 1
if PERF:
    holes = cq.Workplane("XY").pushPoints(pts).circle(HOLE_D/2).extrude(FLOOR + 0.2)
    tray = tray.cut(holes.translate((0, 0, -0.1)))

# board B down-camera hole (rear, on the pad edge)
tray = tray.cut(cq.Workplane("XY").center(-40.0, 0).circle(CAM_B_D/2).extrude(FLOOR + 0.2).translate((0, 0, -0.1)))

# board A forward window through the front wall (+x)
win = (cq.Workplane("YZ").workplane(offset=L/2 - WALL - 0.1)
       .center(0, FLOOR + CAM_A_Z).circle(CAM_A_D/2).extrude(WALL + 0.2))
tray = tray.cut(win)

# outrigger rod holes through both side walls (y = ±W/2)
for rx in ROD_X:
    rod = (cq.Workplane("XZ").workplane(offset=-(W/2 + 1))
           .center(rx, ROD_Z).circle(ROD_D/2).extrude(W + 2))
    tray = tray.cut(rod)

# cable notches on the rear wall top
for ny in (-25.0, 25.0):
    notch = cq.Workplane("XY").center(-L/2, ny).box(2*WALL + 0.2, 5.0, 4.0, centered=(True, True, False)).translate((0, 0, H - 4.0))
    tray = tray.cut(notch)

# hanging ears with Ø2 holes at the four corners, level with the top rim
for sx in (-1, 1):
    for sy in (-1, 1):
        ear = (cq.Workplane("XY").workplane(offset=H - 1.2)
               .center(sx*(L/2 + EAR/2 - 0.5), sy*(W/2 - 6.0))
               .rect(EAR + 1.0, 8.0).extrude(1.2)
               .faces(">Z").workplane().hole(2.0))
        tray = tray.union(ear)


# ---- the face: ◕‿◕‿⚆ embossed on the front wall; the camera window is the middle eye ----
# raised 0.6 mm proud of the wall, on the YZ plane at x = +L/2. y runs across the wall.
FACE_Z = FLOOR + CAM_A_Z            # eye line = camera window height
EYE_R, RING_W, EMB = CAM_A_D/2, 1.2, 0.6
def yz(): return cq.Workplane("YZ").workplane(offset=L/2)
def ring(y, r_out, r_in): return yz().center(y, FACE_Z).circle(r_out).circle(r_in).extrude(EMB)
def dot(y, r):            return yz().center(y, FACE_Z).circle(r).extrude(EMB)
def smile(y):                       # lower half of a thin annulus = ‿
    arc = yz().center(y, FACE_Z + 1.0).circle(3.4).circle(2.4).extrude(EMB)
    keep = cq.Workplane("XY").box(EMB + 1, 8, 4, centered=(True, True, False)).translate((L/2 + EMB/2, y, FACE_Z - 3.0))
    return arc.intersect(keep)
face = (ring(-22, EYE_R, EYE_R - RING_W).union(dot(-22, 1.8))            # ◕ left: ring + big pupil
        .union(ring(0, EYE_R + 0.9, EYE_R + 0.9 - RING_W))               # ◕ middle: ring round the camera window
        .union(ring(22, EYE_R, EYE_R - RING_W)).union(dot(22, 0.9))      # ⚆ right: ring + small pupil
        .union(smile(-11)).union(smile(11)))                             # ‿ ‿
tray = tray.union(face)

# ---- motor clip (×3: M1, M2 on the rods; M3 glued under the floor centre) ----
# ring for the motor, axis along +x (thrust), with a cross-hole for the rod along y
# and a small flat flange so the same part can be glued flat for M3.
clip = (cq.Workplane("YZ").circle(MOTOR_D/2 + 1.2).extrude(CLIP_H).translate((-CLIP_H/2, 0, 0)))
clip = clip.cut(cq.Workplane("YZ").circle(MOTOR_D/2).extrude(CLIP_H + 2).translate((-CLIP_H/2 - 1, 0, 0)))
clip = clip.cut(cq.Workplane("XY").box(CLIP_H + 2, 1.0, MOTOR_D + 4).translate((0, 0, MOTOR_D/2 + 1)))   # grip slit on top
saddle = cq.Workplane("XY").box(CLIP_H, 8.0, 4.0).translate((0, 0, -(MOTOR_D/2 + 1.2 + 2.0) + 0.6))
clip = clip.union(saddle)
clip = clip.cut(cq.Workplane("XZ").workplane(offset=-6).center(0, -(MOTOR_D/2 + 1.2 + 1.4) + 0.6).circle(ROD_D/2).extrude(12))

# ---- export --------------------------------------------------------------------
if EXPORT:
    cq.exporters.export(tray, "tray.stl", tolerance=0.02, angularTolerance=0.1)
    cq.exporters.export(clip, "motor_clip.stl", tolerance=0.02, angularTolerance=0.1)
    cq.exporters.export(tray, "tray_top.svg", opt={"projectionDir": (0, 0, 1), "width": 900, "height": 560, "showAxes": False, "showHidden": False, "strokeWidth": 0.6})
    cq.exporters.export(tray, "tray_side.svg", opt={"projectionDir": (0, -1, 0), "width": 900, "height": 240, "showAxes": False, "showHidden": True, "strokeWidth": 0.6})
    cq.exporters.export(tray, "tray_iso.svg", opt={"projectionDir": (1, -0.6, 0.45), "width": 900, "height": 560, "showAxes": False, "showHidden": False, "strokeWidth": 0.6})
    cq.exporters.export(tray, "tray_face.svg", opt={"projectionDir": (1, 0, 0), "width": 900, "height": 200, "showAxes": False, "showHidden": False, "strokeWidth": 0.7})
    cq.exporters.export(clip, "clip_iso.svg", opt={"projectionDir": (1, -1, 0.8), "width": 400, "height": 320, "showAxes": False, "showHidden": False, "strokeWidth": 0.6})

    vol = tray.val().Volume() / 1000.0   # cm³
    print(f"tray volume {vol:.1f} cm³ → PLA ≈ {vol*1.24:.1f} g, PETG ≈ {vol*1.27:.1f} g")
    cv = clip.val().Volume() / 1000.0
    print(f"clip volume {cv:.2f} cm³ → PLA ≈ {cv*1.24:.2f} g each, ×3")
    print(f"outer {L}×{W}×{H} mm · wall {WALL} · floor {FLOOR} · {len(pts)} floor holes Ø{HOLE_D}")
    print("layout (+x = forward): board A vertical at +x wall (window z=%.1f) · battery A on +x pad · M3 boss at 0,0 under floor"
          " · battery B on −x pad · board B flat at x=-40 over the Ø%.0f down-camera hole · rods at x=%s, z=%.1f" % (FLOOR+CAM_A_Z, CAM_B_D, ROD_X, ROD_Z))
