"""Wibwob Blimp v0 — full prototype assembly for drawings: two 36" latex balloons at ~24" fill,
yoke rod, tray (from tray.py), boards, batteries, drivers, three motors with props, guy lines.
Run: uv run --with cadquery python assembly.py   → assembly_*.svg (+ png via chrome)
Dimensions mm. Not for printing; for looking."""
import os, runpy, cadquery as cq
os.environ["TRAY_EXPORT"] = "0"; os.environ["TRAY_PERF"] = "0"
T = runpy.run_path("tray.py")
tray, clip = T["tray"], T["clip"]
L, W, H, ROD_X, ROD_Z = T["L"], T["W"], T["H"], T["ROD_X"], T["ROD_Z"]

BAL_R = 300.0                 # 24" fill ≈ 60 cm sphere
GAP = 8.0                     # between balloons at the yoke
BAL_Y = BAL_R + GAP/2
NECK = 40.0                   # neck stub below the sphere
YOKE_Z = -(BAL_R + NECK)      # yoke rod sits at the knots
TRAY_Z = YOKE_Z - 60.0        # tray top rim 60 mm below the yoke

# balloons + necks
bal = None
for sy in (-1, 1):
    b = cq.Workplane("XY").sphere(BAL_R).translate((0, sy*BAL_Y, 0))
    n = cq.Workplane("XY").circle(6).extrude(NECK).translate((0, sy*BAL_Y, -BAL_R - NECK + 6))
    knot = cq.Workplane("XY").sphere(7).translate((0, sy*BAL_Y, YOKE_Z))
    bal = b.union(n).union(knot) if bal is None else bal.union(b).union(n).union(knot)

# yoke rod between the knots, and the drop loop to the tray
yoke = cq.Workplane("XZ").circle(1.5).extrude(2*BAL_Y + 30).translate((0, BAL_Y + 15, YOKE_Z))
drop = cq.Workplane("XY").circle(0.6).extrude(-(YOKE_Z - TRAY_Z)).translate((0, 0, YOKE_Z))

# tray + contents
tray_a = tray.translate((0, 0, TRAY_Z - H))
boardA = cq.Workplane("XY").box(2.0, 21, 17.8).translate((L/2 - 3.5, 0, TRAY_Z - H + 1 + 9))       # vertical at the nose
camA   = cq.Workplane("YZ").circle(4).extrude(6).translate((L/2 - 2.5, 0, TRAY_Z - H + 6.8))
boardB = cq.Workplane("XY").box(21, 17.8, 2.0).translate((-40, 0, TRAY_Z - H + 1.8))               # flat at the rear
batA   = cq.Workplane("XY").box(17, 59, 7).translate((L/2 - 12, 0, TRAY_Z - H + 4.3))
batB   = cq.Workplane("XY").box(17, 59, 7).translate((-L/2 + 12, 0, TRAY_Z - H + 4.3))
drv1   = cq.Workplane("XY").box(20, 13, 3).translate((-9, 22, TRAY_Z - H + 2.3))
drv2   = cq.Workplane("XY").box(20, 13, 3).translate((9, 22, TRAY_Z - H + 2.3))

# outrigger rods through the walls, motors + props on the ends (thrust along +x)
rods = None
for rx in ROD_X:
    r = cq.Workplane("XZ").circle(1.0).extrude(200).translate((rx, 100, TRAY_Z - H + ROD_Z))
    rods = r if rods is None else rods.union(r)
def motor_x(y):     # motor axis along x, sitting on both rods
    m = cq.Workplane("YZ").circle(4.25).extrude(20).translate((-10, y, TRAY_Z - H + ROD_Z - 7))
    prop = cq.Workplane("YZ").rect(55, 3).extrude(1).translate((11, y, TRAY_Z - H + ROD_Z - 7))
    c = clip.rotate((0,0,0),(0,0,1),0).translate((0, y, TRAY_Z - H + ROD_Z - 7 + 0))
    return m.union(prop)
m1, m2 = motor_x(-97), motor_x(97)
m3 = cq.Workplane("XY").circle(4.25).extrude(-20).translate((0, 0, TRAY_Z - H)).union(
     cq.Workplane("XY").rect(55, 3).extrude(-1).translate((0, 0, TRAY_Z - H - 21)))

# guy lines: tray ends up to each balloon's inner face
guys = None
for sx in (-1, 1):
    for sy in (-1, 1):
        p0 = cq.Vector(sx*(L/2), sy*(W/2 - 6), TRAY_Z)
        p1 = cq.Vector(sx*60, sy*(BAL_Y - BAL_R*0.55), YOKE_Z + BAL_R*0.3)
        g = cq.Workplane("XY").add(cq.Edge.makeLine(p0, p1))
        wire = cq.Workplane("XY").circle(0.5).sweep(g)
        guys = wire if guys is None else guys.union(wire)

soft = bal.union(yoke).union(drop).union(guys)
hard = tray_a.union(boardA).union(camA).union(boardB).union(batA).union(batB).union(drv1).union(drv2).union(rods).union(m1).union(m2).union(m3)
whole = soft.union(hard)


# the room: floor with a tile grid, two far walls, a door; blimp floats at head height
FLOOR_Z = YOKE_Z - 1400.0
RM = 3000.0
floor = cq.Workplane("XY").box(RM, RM, 2).translate((0, 0, FLOOR_Z))
grid = None
for i in range(-2, 3):
    gx = cq.Workplane("XY").box(RM, 3, 3).translate((0, i*600, FLOOR_Z + 2))
    gy = cq.Workplane("XY").box(3, RM, 3).translate((i*600, 0, FLOOR_Z + 2))
    grid = gx.union(gy) if grid is None else grid.union(gx).union(gy)
wall_back = cq.Workplane("XY").box(2, RM, 2400).translate((-RM/2, 0, FLOOR_Z + 1200))
wall_side = cq.Workplane("XY").box(RM, 2, 2400).translate((0, RM/2, FLOOR_Z + 1200))
door = cq.Workplane("XY").box(6, 800, 2000).translate((-RM/2 + 4, -600, FLOOR_Z + 1000))
window = cq.Workplane("XY").box(1000, 6, 1000).translate((300, RM/2 - 4, FLOOR_Z + 1500))
room = floor.union(grid).union(wall_back).union(wall_side).union(door).union(window)
scene = whole.union(room)

views = {"assembly_iso": (1, -0.7, 0.5), "assembly_front": (1, 0, 0.05), "assembly_side": (0, -1, 0.05), "assembly_top": (0, 0, 1),
         "gondola_iso": (1, -0.7, 0.5), "room_iso": (1, -1, 0.6), "room_threequarter": (1, -0.45, 0.3)}
for name, d in views.items():
    obj = hard if name.startswith("gondola") else (scene if name.startswith("room") else whole)
    w, h = (900, 700) if not name.startswith("gondola") else (900, 500)
    if name.startswith("room"): w, h = (1200, 900)
    cq.exporters.export(obj, f"{name}.svg", opt={"projectionDir": d, "width": w, "height": h, "showAxes": False, "showHidden": False, "strokeWidth": (1.1 if name.startswith("room") else 0.5), "strokeColor": (20,20,20)})
    print("wrote", name)
cq.exporters.export(whole, "assembly.step")
