"""
BuildAstrograph.py — Fusion 360 script.

Creates the 200 mm f/4 astrograph as a parametric Fusion document:
  1. a NEW document (never touches whatever you have open)
  2. the full driving-parameter set as User Parameters
  3. the verified assembly geometry imported from STEP
  4. a rigid group so it behaves as one assembly

INSTALL
  Fusion > Utilities > ADD-INS > Scripts and Add-Ins > Scripts > green "+"
  Point it at this folder. Run "BuildAstrograph".

WHY STEP AND NOT NATIVE FEATURES
  The geometry is authored parametrically in build123d and verified by an exact
  pairwise interference check (378 pairs, 0 interferences). Re-deriving 28 parts
  as native Fusion features would duplicate that work and lose the verification.
  The User Parameters below carry the design intent so the model stays readable
  and drivable inside Fusion; edit ota_model.py and re-export to change geometry.

UNIT TRAP
  Fusion's API works in CENTIMETRES internally. Every value here is written as a
  units-tagged expression string ("250 mm") so nothing silently becomes 250 cm.

RATIO TRAP
  ap_fratio must be unitless. If it is ever created as a length it poisons every
  expression that multiplies by it. The expressions below divide the unit out
  explicitly — ap_D * (ap_fratio / 1 mm) — which is robust either way.
"""
import adsk.core, adsk.fusion, traceback, os

STEP_ASSEMBLY = os.path.expanduser("~/astro-lab/ota-cad/assembly/out/ASSEMBLY.step")

PARAMS = [
    ("ap_D",           "200 mm",                                  "mm", "clear aperture"),
    ("ap_fratio",      "4",                                       "",   "focal ratio (unitless)"),
    ("ap_F",           "ap_D * (ap_fratio / 1 mm)",               "mm", "focal length = 800"),
    ("ap_sec_minor",   "80 mm",                                   "mm", "secondary minor axis"),
    ("ap_sec_major",   "114 mm",                                  "mm", "secondary major axis"),
    ("ap_sec_thk",     "18 mm",                                   "mm", "secondary thickness"),
    ("ap_sec_offset",  "ap_sec_minor / (4 * (ap_fratio / 1 mm))", "mm", "offset toward primary = 5.0"),
    ("ap_mir_dia",     "200 mm",                                  "mm", "primary diameter"),
    ("ap_mir_thk",     "24 mm",                                   "mm", "primary thickness, GSO AD048"),
    ("ap_tube_od",     "250 mm",                                  "mm", "tube outside diameter"),
    ("ap_tube_wall",   "1.5 mm",                                  "mm", "wall - FEA authorised"),
    ("ap_tube_len",    "720 mm",                                  "mm", "tube length"),
    ("ap_backfocus",   "110 mm",                                  "mm", "corrector backfocus"),
    ("ap_focuser_h",   "35 mm",                                   "mm", "focuser height above OD"),
    ("ap_l",           "ap_tube_od / 2 + ap_focuser_h + ap_backfocus", "mm", "axis to focal plane = 270"),
    ("ap_z_spider",    "ap_F - ap_l",                             "mm", "primary vertex to secondary = 530"),
    ("ap_cell_base",   "29 mm",                                   "mm", "cell underside station"),
    ("ap_pad_top",     "ap_cell_base + 17 mm",                    "mm", "pad face = primary vertex = 46"),
    ("ap_z_sec",       "ap_pad_top + ap_z_spider",                "mm", "secondary station = 576"),
    ("ap_z_vane",      "ap_z_sec + 70 mm",                        "mm", "vane plane, skyward of secondary"),
    ("ap_ring1",       "210 mm",                                  "mm", "tube ring 1 station"),
    ("ap_ring2",       "450 mm",                                  "mm", "tube ring 2 station"),
    ("fit_mirror_rad", "0.75 mm",                                 "mm", "glass OD to edge roller face"),
    ("fit_clip_proud", "0.40 mm",                                 "mm", "clip standoff above glass"),
    ("fit_cell_tube",  "0.55 mm",                                 "mm", "cell OD to tube ID per side"),
    ("fit_ring_tube",  "0.25 mm",                                 "mm", "ring bore to tube OD per side"),
    ("fit_print",      "0.25 mm",                                 "mm", "FDM mating clearance per side"),
]

def run(context):
    ui = None
    try:
        app = adsk.core.Application.get()
        ui = app.userInterface

        doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
        des = adsk.fusion.Design.cast(app.activeProduct)
        des.designType = adsk.fusion.DesignTypes.ParametricDesignType
        root = des.rootComponent

        failures = []
        for name, expr, unit, comment in PARAMS:
            existing = des.userParameters.itemByName(name)
            try:
                if existing:
                    existing.expression = expr
                else:
                    des.userParameters.add(
                        name, adsk.core.ValueInput.createByString(expr), unit, comment)
            except Exception as e:
                failures.append("%s: %s" % (name, e))

        if not os.path.exists(STEP_ASSEMBLY):
            ui.messageBox("Assembly STEP not found:\n%s\n\n"
                          "Run ota_model.py then assembly/parts.py to export it."
                          % STEP_ASSEMBLY)
            return
        opts = app.importManager.createSTEPImportOptions(STEP_ASSEMBLY)
        app.importManager.importToTarget(opts, root)

        occs = adsk.core.ObjectCollection.create()
        for o in root.occurrences:
            occs.add(o)
        if occs.count:
            rg = root.rigidGroups.add(occs, True)
            rg.name = "OTA_ASSEMBLY"

        vol = sum(sum(b.volume for b in o.bRepBodies) for o in root.occurrences)
        vol += sum(b.volume for b in root.bRepBodies)

        cam = app.activeViewport.camera
        cam.viewOrientation = adsk.core.ViewOrientations.IsoTopRightViewOrientation
        cam.isFitView = True
        app.activeViewport.camera = cam

        msg = ("Astrograph assembly built.\n\n"
               "Parameters : %d\nOccurrences: %d\nVolume     : %.1f cm3\n"
               "(expected 4741.2 cm3 from the verified model)\n" %
               (des.userParameters.count, root.occurrences.count, vol))
        if failures:
            msg += "\nParameter failures:\n" + "\n".join(failures)
        ui.messageBox(msg)

    except:
        if ui:
            ui.messageBox("Failed:\n{}".format(traceback.format_exc()))
