"""200 f/4 Newtonian astrograph — parametric OTA model.
Driven by the same parameters as scope_design.py. Change a parameter, regenerate.

  ~/astro-lab/.venv/bin/python ota_model.py
"""
import math, os
from build123d import *

# ============================ DESIGN PARAMETERS ============================
D            = 200.0    # clear aperture
FRATIO       = 4.0
F            = D * FRATIO                     # 800 focal length
SEC_MINOR    = 80.0     # secondary minor axis (calc said 78.1 -> next size up)
SEC_MAJOR    = 114.0    # GSO AD093
SEC_THK      = 18.0
SEC_OFFSET   = SEC_MINOR / (4 * FRATIO)       # 5.0 mm toward primary, away from focuser

MIR_DIA      = 200.0    # GSO AD048 actual
MIR_THK      = 24.0     # GSO actual (not 25)
MIR_MASS     = 1.79     # kg, GSO published

TUBE_OD      = 250.0
TUBE_WALL    = 1.5     # FEA-authorised (0.0015 mm vs 1.41 mm budget)
TUBE_ID      = TUBE_OD - 2 * TUBE_WALL
FOCUSER_H    = 35.0     # tube OD surface to drawtube face (low profile)
BACKFOCUS    = 110.0    # drawtube face to sensor (corrector 55 + body + insertion)
L            = TUBE_OD / 2 + FOCUSER_H + BACKFOCUS      # optical axis -> focal plane

PRI_TO_SEC   = F - L                          # 530 mm along the tube axis
CELL_STANDOFF= 30.0     # primary vertex above tube base
TUBE_BEYOND  = 160.0    # tube past the secondary (stray light)
TUBE_LEN     = CELL_STANDOFF + PRI_TO_SEC + TUBE_BEYOND

# --- the decision that removes the corrector dependency --------------------
CELL_ADJ     = 15.0     # +/- axial adjustment of the primary cell
# tolerances (from the design tolerance schedule)
MIR_RADIAL_CLR = 0.75   # 0.5-1.0 mm
CLIP_PROUD     = 0.30
FAN_DIA        = 120.0
VANE_T         = 0.5

OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "exports")

# ============================ 1. MIRROR CELL ===============================
CELL_OD   = TUBE_ID - 1.0
CELL_THK  = 12.0
PAD_R     = MIR_DIA / 2 * 0.7071        # 3-point support at 0.707 R (min deflection)

with BuildPart() as cell:
    Cylinder(CELL_OD/2, CELL_THK)
    # central bore for the 120 mm cooling fan
    Cylinder(FAN_DIA/2, CELL_THK, mode=Mode.SUBTRACT)
    # 3 collimation screw bosses at 120 deg
    with PolarLocations(CELL_OD/2 - 14, 3):
        Cylinder(6, CELL_THK + 8, align=(Align.CENTER, Align.CENTER, Align.MIN))
    with PolarLocations(CELL_OD/2 - 14, 3):
        Cylinder(2.55, CELL_THK + 12, mode=Mode.SUBTRACT)      # M6 tapping drill
    # 3 mirror support pads at 0.707 R, offset 60 deg from the collimation screws
    with PolarLocations(PAD_R, 3, start_angle=60):
        Cylinder(11, CELL_THK + 5, align=(Align.CENTER, Align.CENTER, Align.MIN))
    # fan mounting holes, 105 mm square pattern
    for x in (-52.5, 52.5):
        for y in (-52.5, 52.5):
            with Locations((x, y, 0)):
                Cylinder(2.2, CELL_THK + 4, mode=Mode.SUBTRACT)
    # axial adjustment slots - THIS is what frees you from the corrector
    with PolarLocations(CELL_OD/2 - 4, 3, start_angle=30):
        Box(9, 5.5, CELL_THK + 4, mode=Mode.SUBTRACT)

# ============================ 2. SPIDER ====================================
# 4 vanes, secondary offset built into the hub (the step most builds forget)
HUB_D, HUB_H = 44.0, 26.0
VANE_L = (TUBE_ID - HUB_D) / 2 + 6

with BuildPart() as spider:
    with Locations((0, SEC_OFFSET, 0)):          # <-- OFFSET IS IN THE GEOMETRY
        Cylinder(HUB_D/2, HUB_H)
        Cylinder(5.0, HUB_H + 4, mode=Mode.SUBTRACT)   # M10 secondary stalk
    for ang in (0, 90, 180, 270):
        with Locations(Rotation(0, 0, ang) * Location((0, TUBE_ID/4, 0))):
            Box(VANE_T, VANE_L, 22)

# ============================ 3. SECONDARY HOLDER ==========================
with BuildPart() as sec_holder:
    # 45 deg elliptical seat, sized to the GSO AD093
    Cylinder(SEC_MAJOR/2 + 5, 14)
    with Locations((0, 0, 14)):
        Box(SEC_MAJOR + 12, SEC_MINOR + 12, SEC_THK + 3,
            align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT)
    # 3 tilt-adjust screws
    with PolarLocations(SEC_MAJOR/2 - 6, 3):
        Cylinder(1.7, 20, mode=Mode.SUBTRACT)
    # central stalk boss
    Cylinder(9, 26, align=(Align.CENTER, Align.CENTER, Align.MAX))

# ============================ 4. FOCUSER BOARD =============================
# Machined, NOT hand-cut. Squareness to tube axis is the whole point.
with BuildPart() as focboard:
    Box(120, 120, 14)
    # concave face matching the tube OD so it seats without rocking
    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.0, 40, mode=Mode.SUBTRACT)          # 2in drawtube clearance
    with PolarLocations(46, 4, start_angle=45):
        Cylinder(2.6, 30, mode=Mode.SUBTRACT)       # M5 focuser fixings

# ============================ 5. TUBE ======================================
with BuildPart() as tube:
    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)
    # focuser aperture, at the secondary station
    with Locations(Location((0, 0, CELL_STANDOFF + PRI_TO_SEC), (0, 90, 0))):
        Cylinder(30.5, TUBE_OD, mode=Mode.SUBTRACT)
    # spider vane slots
    for ang in (0, 90, 180, 270):
        with Locations(Location((0, 0, CELL_STANDOFF + PRI_TO_SEC),
                                (0, 0, ang)) * Location((0, TUBE_OD/2, 0), (90, 0, 0))):
            Box(VANE_T + 0.4, 24, 12, mode=Mode.SUBTRACT)

# ============================ EXPORT =======================================
parts = {"mirror_cell": cell.part, "spider": spider.part,
         "secondary_holder": sec_holder.part, "focuser_board": focboard.part
         if False else focboard.part, "tube": tube.part}
os.makedirs(OUT, exist_ok=True)
print("=" * 70)
print("  200 f/4 ASTROGRAPH — PARAMETRIC OTA")
print("=" * 70)
for name, p in parts.items():
    export_step(p, f"{OUT}/{name}.step")
    if name != "tube":
        export_stl(p, f"{OUT}/{name}.stl")
    print(f"  {name:<20} vol {p.volume/1000:>9.1f} cm3   alu {p.volume*2.70e-3/1000:>6.2f} kg")

# ============================ VERIFICATION =================================
print("\n" + "=" * 70)
print("  GEOMETRY CHECK")
print("=" * 70)
checks = []
checks.append(("l (axis -> focal plane)", L, "240-280 mm for an 80 mm secondary", 240 <= L <= 280))
m_req = L * D / F + 16.0 * (F - L) / F
checks.append(("secondary required", m_req, f"<= {SEC_MINOR:.0f} mm fitted", m_req <= SEC_MINOR))
checks.append(("secondary offset", SEC_OFFSET, "m/(4N), toward primary", True))
checks.append(("primary -> secondary", PRI_TO_SEC, "F - l", True))
checks.append(("tube length", TUBE_LEN, "cf. GSO 8in f/4 OTA = 720 mm", abs(TUBE_LEN-720) < 40))
checks.append(("mirror radial clearance", (TUBE_ID - MIR_DIA)/2, ">= 0.75 mm, cell sets final", True))
checks.append(("cell axial adjust", CELL_ADJ, "+/- mm, frees corrector dependency", CELL_ADJ >= 10))
for label, val, note, ok in checks:
    print(f"  {'PASS' if ok else 'FAIL'}  {label:<26}{val:>8.1f}   {note}")

alu = sum(p.volume for n, p in parts.items()) * 2.70e-3 / 1000
print(f"\n  Fabricated aluminium mass      {alu:>6.2f} kg")
print(f"  + primary {MIR_MASS} + secondary 0.33 + focuser 0.75 + rings/dovetail 1.30")
print(f"  + camera/corrector/OAG/EAF 1.85")
print(f"  = OTA + imaging train         {alu + MIR_MASS + 0.33 + 0.75 + 1.30 + 1.85:>6.2f} kg")
print(f"  Budgeted in the design spec:   10.00 kg")
print(f"\n  STEP -> Fusion / machinist.  STL -> H2C for spider, holder, cell.")
print(f"  Files: {OUT}")
