"""Full OTA part library — every part modelled, positioned for assembly.
Units mm. Positions are in ASSEMBLY space: tube axis = Z, primary vertex at Z=41.
"""
import math
from build123d import *

def vane_cutter(thk=1.1, extra=4.0):
    """A thicker copy of the vane, same footprint - cuts a guaranteed-fit slot."""
    L = VANE_TIP - VANE_ROOT
    with BuildPart() as b:
        Box(thk, L + extra, 22 + extra, align=(Align.CENTER, Align.MIN, Align.CENTER))
    return b.part

# ═══════════════ MASTER PARAMETERS ═══════════════
D, FRATIO      = 200.0, 4.0
F              = D * FRATIO                 # 800
SEC_MINOR, SEC_MAJOR, SEC_THK = 80.0, 114.0, 18.0
SEC_OFFSET     = SEC_MINOR / (4*FRATIO)     # 5.0
MIR_DIA, MIR_THK = 200.0, 24.0
TUBE_OD, TUBE_WALL = 250.0, 1.5
TUBE_ID        = TUBE_OD - 2*TUBE_WALL      # 247.0
TUBE_LEN       = 720.0
CELL_STANDOFF  = 30.0
Z_PRIMARY      = 41.0                       # primary front face
Z_SPIDER       = 560.0
Z_RING1, Z_RING2 = 210.0, 450.0
Z_GLASS_FRONT  = 29.0 + 17.0 + 24.0      # pads + glass thickness
Z_VANE         = 560.0 + 70.0   # vane plane, skyward of the secondary
STALK_BOT      = 560.0 + 34.0           # clear of the tilted holder's rear face
STALK_TOP      = Z_VANE - 13.0          # hub underside

# ── TOLERANCE / FIT SCHEDULE (single source of truth) ──
FIT = {
 "mirror_radial":      (0.50, 1.00),   # glass OD to cell wall, per side
 "clip_standoff":      (0.30, 0.50),   # clip face above glass front
 "cell_to_tube":       (0.30, 0.80),   # cell OD to tube ID, per side
 "vane_slot":          (0.05, 0.15),   # vane thickness to hub slot
 "sec_seat":           (0.20, 0.60),   # secondary edge to holder seat
 "ring_to_tube":       (0.10, 0.40),   # ring bore to tube OD, per side
 "focuser_bore":       (0.25, 0.75),   # drawtube to board bore, per side
 "print_clearance":    (0.20, 0.35),   # FDM mating clearance, per side
 "envelope_min":       (60.0, None),   # optics swept envelope to tube ID
}
PRINTED = {"secondary_holder", "mirror_clip", "edge_roller", "fan_shroud",
           "dew_shield_half", "cable_guide"}
# tube_ring deliberately NOT printed: 285 mm PA-CF warps on the plate and creeps
# under sustained load, and the rings carry the whole OTA into the dovetail.
H2C = (305.0, 320.0, 325.0); H2C_MARGIN = 5.0

# ═══════════════ PARTS ═══════════════
def p_tube():
    with BuildPart() as b:
        Cylinder(TUBE_OD/2, TUBE_LEN, align=(Align.CENTER,Align.CENTER,Align.MIN))
        Cylinder(TUBE_ID/2, TUBE_LEN, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
        with Locations(Location((0,0,Z_SPIDER),(0,90,0))):
            Cylinder(30.5, TUBE_OD, mode=Mode.SUBTRACT)
    tube = b.part
    for a in (45,135,225,315):      # slot cut from a thicker copy of the vane, same position
        c = vane_cutter().translate((0, VANE_ROOT, 0)).rotate(Axis.Z, a) \
                         .translate((0, SEC_OFFSET, Z_VANE))
        tube = tube - c
    return tube

def p_mirror_cell():
    OD = TUBE_ID - 2*0.55          # cell_to_tube fit, mid of band
    with BuildPart() as b:
        Cylinder(OD/2, 12)
        Cylinder(60, 12, mode=Mode.SUBTRACT)
        with PolarLocations(OD/2-14, 3):
            Cylinder(6, 20, align=(Align.CENTER,Align.CENTER,Align.MIN))
        with PolarLocations(70.71, 3, start_angle=60):
            Cylinder(11, 17, align=(Align.CENTER,Align.CENTER,Align.MIN))
        with PolarLocations(OD/2-14, 3):
            Cylinder(2.55, 26, mode=Mode.SUBTRACT)
        with PolarLocations(OD/2-4, 3, start_angle=30):
            Box(9, 5.5, 20, mode=Mode.SUBTRACT)
    return b.part

def p_primary():
    with BuildPart() as b: Cylinder(MIR_DIA/2, MIR_THK)
    return b.part

def p_mirror_clip():          # PRINTED PAHT-CF
    with BuildPart() as b:
        Box(26, 14, 6, align=(Align.CENTER,Align.MIN,Align.MIN))
        with Locations((0,4,6)): Box(26, 6, 5, align=(Align.CENTER,Align.MIN,Align.MIN))
        with Locations((0,8,0)): Cylinder(1.7, 20, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
    return b.part

def p_edge_roller():          # PRINTED PAHT-CF, PTFE roller pocket
    with BuildPart() as b:
        Box(16, 14, 14, align=(Align.CENTER,Align.CENTER,Align.MIN))
        with Locations((0,0,14)):
            Cylinder(5.2, 24, rotation=(90,0,0), mode=Mode.SUBTRACT)
        with Locations((0,0,3)): Cylinder(2.2, 20, mode=Mode.SUBTRACT)
    return b.part

def p_spider_hub():
    with BuildPart() as b:
        Cylinder(22, 26)
        Cylinder(5.0, 34, mode=Mode.SUBTRACT)
        for a in (45,135,225,315):   # vane slots, 0.62 for a 0.5 vane (0.06/side)
            with Locations(Location((0,0,0),(0,0,a)) * Location((0,18,0))):
                Box(0.62, 10, 22, mode=Mode.SUBTRACT)
    return b.part

VANE_ROOT = 14.0        # starts inside the hub slot, clear of the hub bore
VANE_TIP  = 126.0       # reaches into the tube-wall slot at OD/2 = 125
def p_spider_vane():
    L = VANE_TIP - VANE_ROOT
    with BuildPart() as b:
        Box(0.5, L, 22, align=(Align.CENTER,Align.MIN,Align.CENTER))
        with Locations((0,L-8,0)): Box(4, 10, 4, mode=Mode.SUBTRACT)
    return b.part

def p_secondary():
    with BuildPart() as b:
        Cylinder(SEC_MINOR/2, SEC_THK)
        scale(by=(SEC_MAJOR/SEC_MINOR, 1, 1))
    return b.part

def p_secondary_holder():     # PRINTED PAHT-CF
    with BuildPart() as b:
        Box(SEC_MAJOR+12, SEC_MINOR+12, 24, align=(Align.CENTER,Align.CENTER,Align.MIN))
        with BuildPart(mode=Mode.SUBTRACT):
            with Locations((0,0,(SEC_THK+2)/2)):
                Cylinder((SEC_MINOR+2*0.4)/2, SEC_THK+2)
                scale(by=((SEC_MAJOR+2*0.4)/(SEC_MINOR+2*0.4), 1, 1))
        with PolarLocations(SEC_MAJOR/2-4, 3):
            Cylinder(1.7, 24, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
        with Locations((0,0,24)):
            Cylinder(11, 8, align=(Align.CENTER,Align.CENTER,Align.MIN))
            Cylinder(5.2, 34, align=(Align.CENTER,Align.CENTER,Align.MAX), mode=Mode.SUBTRACT)
    return b.part

def p_secondary_stalk():
    L = STALK_TOP - STALK_BOT
    with BuildPart() as b:
        Cylinder(7.5, L, align=(Align.CENTER,Align.CENTER,Align.MIN))
        Cylinder(4.2, L+8, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
    return b.part

def p_focuser_board():
    with BuildPart() as b:
        Box(120, 120, 14)
        with Locations((0,0,-7)):
            Cylinder(TUBE_OD/2, 140, rotation=(0,90,0),
                     align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
        Cylinder(30.75, 40, mode=Mode.SUBTRACT)
        with PolarLocations(46, 4, start_angle=45):
            Cylinder(2.6, 30, mode=Mode.SUBTRACT)
    return b.part

def p_tube_ring():            # PRINTED PAHT-CF (checked against H2C envelope)
    BORE = TUBE_OD + 2*0.25
    with BuildPart() as b:                 # MACHINED aluminium, not printed
        Cylinder(BORE/2+13, 26)
        Cylinder(BORE/2, 30, mode=Mode.SUBTRACT)
        with Locations((0,-(BORE/2+11.0),0)):
            Box(60, 22, 26)
        with Locations((0,-(BORE/2+11.0),0)):
            with GridLocations(40,0,2,1):
                Cylinder(3.3, 34, mode=Mode.SUBTRACT)
        with Locations((0, BORE/2+6, 0)):
            Box(30, 14, 26, mode=Mode.SUBTRACT)      # hinge/clamp gap
    return b.part

def p_dovetail():
    with BuildPart() as b:
        Box(74, 14, 320, align=(Align.CENTER,Align.CENTER,Align.CENTER))
    return b.part

def p_fan_shroud():           # PRINTED PETG
    with BuildPart() as b:
        Cylinder(66, 18, align=(Align.CENTER,Align.CENTER,Align.MIN))
        Cylinder(60, 24, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
        with GridLocations(105,105,2,2):
            Cylinder(2.3, 24, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
    return b.part

def p_dew_shield_half():      # PRINTED PETG, split because Ø250 x 300 exceeds H2C
    with BuildPart() as b:
        Cylinder(TUBE_OD/2+3.2, 300, align=(Align.CENTER,Align.CENTER,Align.MIN))
        Cylinder(TUBE_OD/2+1.0, 320, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
        with Locations((TUBE_OD/2+20,0,150)): Box(TUBE_OD+40, TUBE_OD+40, 320, mode=Mode.SUBTRACT)
    return b.part

def p_cable_guide():          # PRINTED PETG
    with BuildPart() as b:
        Box(30, 16, 10, align=(Align.CENTER,Align.CENTER,Align.MIN))
        with Locations((0,0,10)): Cylinder(5, 34, rotation=(0,90,0), mode=Mode.SUBTRACT)
        with Locations((0,0,0)): Cylinder(2.2, 16, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
    return b.part

def p_focuser_body():         # bought-in envelope
    with BuildPart() as b:
        Cylinder(38, 35, align=(Align.CENTER,Align.CENTER,Align.MIN))
        with Locations((0,0,35)): Cylinder(30.0, 42, align=(Align.CENTER,Align.CENTER,Align.MIN))
    return b.part

def p_imaging_train():        # corrector + drawer + OAG + camera envelope
    with BuildPart() as b:
        Cylinder(30, 62, align=(Align.CENTER,Align.CENTER,Align.MIN))
        with Locations((0,0,62)): Cylinder(43, 48, align=(Align.CENTER,Align.CENTER,Align.MIN))
    return b.part

def p_fan():
    with BuildPart() as b:
        Box(120, 120, 25, align=(Align.CENTER,Align.CENTER,Align.MIN))
        Cylinder(58, 30, align=(Align.CENTER,Align.CENTER,Align.MIN), mode=Mode.SUBTRACT)
    return b.part

# ═══════════════ ASSEMBLY ═══════════════
def assembly():
    """[(name, solid, printed?)] — every position derived from a mating datum."""
    # datums
    Z_CELL_BASE = 29.0
    Z_PAD_TOP   = Z_CELL_BASE + 17.0          # cell pad face
    Z_GLASS_BOT = Z_PAD_TOP                   # glass SITS on the pads
    Z_GLASS_TOP = Z_GLASS_BOT + MIR_THK
    Z_CLIP_BOT  = Z_GLASS_TOP + 0.40          # clip standoff fit
    R_ROLLER    = 100.75 + 10.6               # roller inner face 0.75 off the glass OD
    Y_RING_FOOT = -(TUBE_OD/2 + 0.25 + 11.0 + 11.0)
    A = []
    def add(n, s_, printed=False): A.append((n, s_, printed)); return s_

    add("tube", p_tube())
    add("mirror_cell", p_mirror_cell().translate((0,0,Z_CELL_BASE)))
    add("primary", p_primary().translate((0,0,Z_GLASS_BOT + MIR_THK/2)))
    for i,a in enumerate((90,210,330)):
        r = math.radians(a)
        add(f"mirror_clip_{i+1}", p_mirror_clip().rotate(Axis.Z, a-90)
            .translate((104*math.cos(r), 104*math.sin(r), Z_CLIP_BOT)), printed=True)
    for i,a in enumerate((225,315)):
        r = math.radians(a)
        add(f"edge_roller_{i+1}", p_edge_roller().rotate(Axis.Z, a)
            .translate((R_ROLLER*math.cos(r), R_ROLLER*math.sin(r), Z_CELL_BASE+12)), printed=True)
    add("fan_shroud", p_fan_shroud().translate((0,0,Z_CELL_BASE-6-18)), printed=True)
    add("fan", p_fan().translate((0,0,Z_CELL_BASE-6-18-25)))

    add("spider_hub", p_spider_hub().translate((0,SEC_OFFSET,Z_VANE)))
    for i,a in enumerate((45,135,225,315)):
        v = p_spider_vane().translate((0, VANE_ROOT, 0)).rotate(Axis.Z, a)
        add(f"spider_vane_{i+1}", v.translate((0,SEC_OFFSET,Z_VANE)))
    add("secondary_holder", p_secondary_holder().rotate(Axis.X, 45)
        .translate((0,SEC_OFFSET,Z_SPIDER)), printed=True)
    _pz = (SEC_THK + 2.0)/2.0                      # pocket centre, holder-local
    add("secondary_stalk", p_secondary_stalk().translate((0,SEC_OFFSET,STALK_BOT)))
    add("secondary", p_secondary().rotate(Axis.X, 45)
        .translate((0, SEC_OFFSET - _pz*math.sin(math.radians(45)),
                       Z_SPIDER + _pz*math.cos(math.radians(45)))))

    add("focuser_board", p_focuser_board().rotate(Axis.Y, 90).translate((TUBE_OD/2+7,0,Z_SPIDER)))
    add("focuser_body", p_focuser_body().rotate(Axis.Y, 90).translate((TUBE_OD/2+14,0,Z_SPIDER)))
    add("imaging_train", p_imaging_train().rotate(Axis.Y, 90).translate((TUBE_OD/2+91,0,Z_SPIDER)))
    add("tube_ring_1", p_tube_ring().translate((0,0,Z_RING1)))
    add("tube_ring_2", p_tube_ring().translate((0,0,Z_RING2)))
    add("dovetail", p_dovetail().translate((0, Y_RING_FOOT - 7.0, (Z_RING1+Z_RING2)/2)))
    add("dew_shield_half_1", p_dew_shield_half().translate((0,0,TUBE_LEN-40)), printed=True)
    add("dew_shield_half_2", p_dew_shield_half().rotate(Axis.Z,180).translate((0,0,TUBE_LEN-40)), printed=True)
    for i,z in enumerate((330,520)):
        add(f"cable_guide_{i+1}", p_cable_guide().rotate(Axis.X,-90)
            .translate((0,-(TUBE_OD/2+14.0),z)), printed=True)
    return A
