master
Colby Knox generate_body pin counts per generator, exposed pad, STEP folder (unreleased, 1.13.4) 0e7aa24 14d ago
[
 {
  "args": {
   "type": "chip",
   "params": {
    "D": 1.6,
    "E": 0.8,
    "A": 0.45,
    "L": 0.3,
    "L1": 0.2
   },
   "etch": "103",
   "outputStep": "C:/tmp/r0603.step"
  },
  "script": "\nimport sys, glob, os, importlib\nimport adsk.core, adsk.fusion\n\n# Locate EPG across install conventions + versions (NEVER hardcode the webdeploy hash).\n_bases = []\nfor env in ('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432', 'LOCALAPPDATA'):\n    root = os.environ.get(env)\n    if root:\n        _bases += glob.glob(os.path.join(root, 'Autodesk', 'webdeploy', 'production', '*',\n                                         'Api', 'InternalAddins', 'ElectronicsPackageGenerator'))\nif not _bases:\n    raise RuntimeError('EPG_NOT_FOUND: ElectronicsPackageGenerator not present in this Fusion install')\nepg_dir = max(_bases, key=os.path.getmtime)\nparent = os.path.dirname(epg_dir)\nif parent not in sys.path:\n    sys.path.insert(0, parent)\n\nmod = importlib.import_module('ElectronicsPackageGenerator.Scripts3d.chip')\n\ndoc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)\ndesign = adsk.fusion.Design.cast(app.activeProduct)\nmod.runWithInput({'D': 0.16, 'E': 0.08, 'A': 0.045, 'L': 0.03, 'L1': 0.02}, design)\nroot = design.rootComponent\n\ninv = [{'name': b.name, 'vol': round(b.volume, 6)} for b in root.bRepBodies]\netched = None\nif '103':\n    # FIND THE TOP SURFACE the robust way (John's algorithm, 2026-07-04):\n    # 1. bounding box of the WHOLE chip (all bodies);\n    # 2. only faces whose centroid sits in the TOP 5% band of that bbox count;\n    # 3. among those upward planar faces take the LARGEST AREA (the molded body\n    #    top beats terminal tops that tie it on height);\n    # 4. lay the text out with a 10% margin inside that face.\n    gmin, gmax = 1e9, -1e9\n    for b in root.bRepBodies:\n        bb = b.boundingBox\n        gmin = min(gmin, bb.minPoint.z)\n        gmax = max(gmax, bb.maxPoint.z)\n    band_z = gmax - 0.05 * (gmax - gmin)\n    top_face, top_area = None, -1.0\n    for b in root.bRepBodies:\n        for f in b.faces:\n            if isinstance(f.geometry, adsk.core.Plane) and f.geometry.normal.z > 0.9                     and f.centroid.z >= band_z and f.area > top_area:\n                top_face, top_area = f, f.area\n    if top_face is None:\n        raise RuntimeError('ETCH_NO_TOP_FACE: no upward planar face in the top 5% of the chip bbox')\n    body = top_face.body\n    sk = root.sketches.add(top_face)\n    center = sk.modelToSketchSpace(top_face.centroid)\n    # Measure the face in SKETCH space (model-space bbox axes can be SWAPPED vs\n    # the sketch axes the text lays out in). Project the model bbox corners.\n    bb = top_face.boundingBox\n    p_min = sk.modelToSketchSpace(bb.minPoint)\n    p_max = sk.modelToSketchSpace(bb.maxPoint)\n    face_w = abs(p_max.x - p_min.x)   # extent along sketch-x (the default text baseline)\n    face_h = abs(p_max.y - p_min.y)   # extent along sketch-y\n    # ALWAYS run the text along the LONGEST axis of the top face (most room for\n    # the marking - John's rule, 2026-07-04). If the long axis is sketch-y,\n    # rotate the text 90deg rather than squeezing it onto the short axis.\n    import math as _math\n    rot_deg = 90 if face_h > face_w else 0\n    long_len = max(face_w, face_h)\n    short_len = min(face_w, face_h)\n    w = long_len * 0.80               # 10% margin each side along the long axis\n    box_h_cap = short_len * 0.80      # 10% margin each side across it\n    # multi-line support (e.g. MPN + variant on line 2): fit per-line\n    lines = '103'.split(chr(10))\n    n_lines = max(1, len(lines))\n    max_chars = max(1, max(len(l) for l in lines))\n    # width-aware auto-fit: glyph ~0.75*h wide; line block ~1.35*h per line\n    h = (None / 10.0) if None else         max(0.015, min(box_h_cap / (1.35 * n_lines), w / (0.75 * max_chars)))\n    if rot_deg:\n        c1 = adsk.core.Point3D.create(center.x - box_h_cap/2, center.y - w/2, 0)\n        c2 = adsk.core.Point3D.create(center.x + box_h_cap/2, center.y + w/2, 0)\n    else:\n        c1 = adsk.core.Point3D.create(center.x - w/2, center.y - box_h_cap/2, 0)\n        c2 = adsk.core.Point3D.create(center.x + w/2, center.y + box_h_cap/2, 0)\n    ti = sk.sketchTexts.createInput2('103', h)\n    ti.setAsMultiLine(c1, c2,\n                      adsk.core.HorizontalAlignments.CenterHorizontalAlignment,\n                      adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)\n    if rot_deg:\n        try:\n            ti.angle = _math.pi / 2.0\n        except Exception:\n            rot_deg = 0  # older API without angle - fall back to unrotated\n    txt = sk.sketchTexts.add(ti)\n    # MEASURE-AND-CORRECT (the margin is a CONTRACT): font metrics vary, so the\n    # 0.75h glyph estimate can under-shoot ('ADOM-A' rendered edge-to-edge). After\n    # placing, measure the text's real bbox; if it busts the 10%-margin box,\n    # recreate at the scaled-down height. Never trust an estimate you can measure.\n    fit_iterations = 0\n    measured_w = None\n    for _pass in range(2):\n        tb = txt.boundingBox\n        t_ext_x = abs(tb.maxPoint.x - tb.minPoint.x)\n        t_ext_y = abs(tb.maxPoint.y - tb.minPoint.y)\n        measured_w = max(t_ext_x, t_ext_y)   # the baseline-axis extent\n        measured_c = min(t_ext_x, t_ext_y)   # the cross-axis extent (line stack)\n        if measured_w <= w and measured_c <= box_h_cap:\n            break\n        scale = min(w / max(measured_w, 1e-6), box_h_cap / max(measured_c, 1e-6)) * 0.95\n        h = max(0.008, h * scale)\n        txt.deleteMe()\n        ti = sk.sketchTexts.createInput2('103', h)\n        ti.setAsMultiLine(c1, c2,\n                          adsk.core.HorizontalAlignments.CenterHorizontalAlignment,\n                          adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)\n        if rot_deg:\n            try:\n                ti.angle = _math.pi / 2.0\n            except Exception:\n                pass\n        txt = sk.sketchTexts.add(ti)\n        fit_iterations += 1\n    marking_geom = {\n        'chipBBox': {'min': [round(gmin,5), 0, 0], 'maxZ': round(gmax,5)},\n        'chipBBoxZ': {'min': round(gmin,5), 'max': round(gmax,5)},\n        'topBandZ': round(band_z,5),\n        'topFace': {'area': round(top_area,6), 'centroidZ': round(top_face.centroid.z,5),\n                    'sketchExtents': {'x': round(face_w,5), 'y': round(face_h,5)},\n                    'body': body.name},\n        'longAxis': 'sketch-y' if face_h > face_w else 'sketch-x',\n        'rotatedDeg': rot_deg,\n        'textBox': {'c1': [round(c1.x,5), round(c1.y,5)], 'c2': [round(c2.x,5), round(c2.y,5)],\n                    'marginPct': 10},\n        'textHeightMm': round(h*10,4), 'lines': n_lines, 'maxLineChars': max_chars,\n        'heightAuto': not bool(None),\n        'textWidthMeasuredMm': round(measured_w*10,4) if measured_w else None,\n        'fitIterations': fit_iterations,\n        'why': {'band': 'top 5% of whole-chip bbox excludes terminal tops that tie the body',\n                'largestArea': 'molded body top beats small terminal faces in the band',\n                'longAxis': 'text runs along the longest face axis for maximum room',\n                'fit': 'h = min(boxH/(1.35*lines), boxW/(0.75*maxChars)) - glyphs ~0.75h wide'},\n    }\n    if 'raised' == 'engraved':\n        # engraved (sunken) cut - the pre-2026-07-04 default\n        ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.CutFeatureOperation)\n        ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(-0.003))\n        ext_in.participantBodies = [body]\n        root.features.extrudeFeatures.add(ext_in)\n    else:\n        # RAISED WHITE marking (default): humans need CONTRAST - a thin positive\n        # extrude painted white on the dark body reads like real silkscreen ink,\n        # far better than a same-color emboss. Reuses EPG's own rgb-appearance\n        # utility so the white survives Fusion rendering (+ colored STEP).\n        ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)\n        ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(0.003))\n        mark = root.features.extrudeFeatures.add(ext_in)\n        from ElectronicsPackageGenerator.Utilities import addin_utility as _au\n        for i in range(mark.bodies.count):\n            mb = mark.bodies.item(i)\n            mb.name = 'Marking' if i == 0 else 'Marking' + str(i + 1)\n            _au.apply_rgb_appearance(app, design, mb, 255, 255, 255, 'AdomMarkingWhite')\n    etched = '103'\n\nstep_path = None\nif 'C:/tmp/r0603.step':\n    # Fusion's STEP export writes nothing into a folder that does not exist: make it first\n    os.makedirs(os.path.dirname(os.path.abspath('C:/tmp/r0603.step')), exist_ok=True)\n    em = design.exportManager\n    opts = em.createSTEPExportOptions('C:/tmp/r0603.step')\n    em.execute(opts)\n    if not os.path.isfile('C:/tmp/r0603.step'):\n        raise RuntimeError('Fusion did not write the STEP file %s' % 'C:/tmp/r0603.step')\n    step_path = 'C:/tmp/r0603.step'\n\n# SIDECAR MANIFEST: every setting we picked + WHY + the bboxes we calculated,\n# so a marking refresh (e.g. adding a variant name as a 2nd line) can rework the\n# text without re-deriving anything. Written next to the STEP as <step>.manifest.json.\nmanifest = {\n    'schema': 'adom-fusion-generate-package/1',\n    'bridgeVersion': 'TESTVER',\n    'type': 'chip',\n    'paramsCm': {'D': 0.16, 'E': 0.08, 'A': 0.045, 'L': 0.03, 'L1': 0.02},\n    'paramsNote': 'paramsCm are EPG-native cm (mm inputs were /10)',\n    'marking': ({'text': '103', 'style': 'raised',\n                'depthMm': 0.003 * 10, **marking_geom} if etched else None),\n    'bodies': inv,\n    'outputs': {'step': step_path},\n    'refresh': {\n        'howTo': 'To re-mark (e.g. add a variant on line 2): call fusion_generate_package again '\n                 'with the SAME type+paramsCm (unitsCm:true) and the new multi-line etch text '\n                 '(join lines with a newline). The generator is deterministic, so geometry and '\n                 'placement reproduce exactly; this manifest carries the boxes/height the last '\n                 'run chose for comparison.',\n        'example': {'type': 'chip', 'unitsCm': True, 'params': 'paramsCm from this file',\n                    'etch': ('103' + chr(10) + 'VARIANT') if etched else 'MPN' + chr(10) + 'VARIANT'},\n    },\n}\nmanifest_path = None\nif step_path:\n    manifest_path = step_path + '.manifest.json'\n    import json as _json\n    with open(manifest_path, 'w', encoding='utf-8') as _f:\n        _json.dump(manifest, _f, indent=2)\n\nvp = app.activeViewport\ncam = vp.camera\ncam.isSmoothTransition = False\ncam.target = adsk.core.Point3D.create(0, 0, 0)\ncam.eye = adsk.core.Point3D.create(0.25, -0.35, 0.45)\ncam.upVector = adsk.core.Vector3D.create(0, 0, 1)\ncam.isFitView = True\nvp.camera = cam\nvp.refresh()\n\nresult = {'type': 'chip', 'bodies': inv, 'etched': etched, 'stepPath': step_path,\n           'manifest': manifest, 'manifestPath': manifest_path,\n           'epgDir': epg_dir, 'doc': app.activeDocument.name}\n",
  "errorCode": null
 },
 {
  "args": {
   "type": "soic",
   "params": {
    "A": 1.75,
    "A1": 0.1,
    "b": 0.41,
    "D": 4.9,
    "E": 6.0,
    "E1": 3.9,
    "e": 1.27,
    "L": 0.8,
    "DPins": 8
   },
   "unitsCm": false,
   "etch": "LM358\nADOM-A",
   "etchStyle": "engraved",
   "etchDepthMm": 0.05,
   "etchHeightMm": 0.5
  },
  "script": "\nimport sys, glob, os, importlib\nimport adsk.core, adsk.fusion\n\n# Locate EPG across install conventions + versions (NEVER hardcode the webdeploy hash).\n_bases = []\nfor env in ('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432', 'LOCALAPPDATA'):\n    root = os.environ.get(env)\n    if root:\n        _bases += glob.glob(os.path.join(root, 'Autodesk', 'webdeploy', 'production', '*',\n                                         'Api', 'InternalAddins', 'ElectronicsPackageGenerator'))\nif not _bases:\n    raise RuntimeError('EPG_NOT_FOUND: ElectronicsPackageGenerator not present in this Fusion install')\nepg_dir = max(_bases, key=os.path.getmtime)\nparent = os.path.dirname(epg_dir)\nif parent not in sys.path:\n    sys.path.insert(0, parent)\n\nmod = importlib.import_module('ElectronicsPackageGenerator.Scripts3d.soic')\n\ndoc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)\ndesign = adsk.fusion.Design.cast(app.activeProduct)\nmod.runWithInput({'A': 0.175, 'A1': 0.01, 'b': 0.040999999999999995, 'D': 0.49000000000000005, 'E': 0.6, 'E1': 0.39, 'e': 0.127, 'L': 0.08, 'DPins': 8}, design)\nroot = design.rootComponent\n\ninv = [{'name': b.name, 'vol': round(b.volume, 6)} for b in root.bRepBodies]\netched = None\nif 'LM358\\nADOM-A':\n    # FIND THE TOP SURFACE the robust way (John's algorithm, 2026-07-04):\n    # 1. bounding box of the WHOLE chip (all bodies);\n    # 2. only faces whose centroid sits in the TOP 5% band of that bbox count;\n    # 3. among those upward planar faces take the LARGEST AREA (the molded body\n    #    top beats terminal tops that tie it on height);\n    # 4. lay the text out with a 10% margin inside that face.\n    gmin, gmax = 1e9, -1e9\n    for b in root.bRepBodies:\n        bb = b.boundingBox\n        gmin = min(gmin, bb.minPoint.z)\n        gmax = max(gmax, bb.maxPoint.z)\n    band_z = gmax - 0.05 * (gmax - gmin)\n    top_face, top_area = None, -1.0\n    for b in root.bRepBodies:\n        for f in b.faces:\n            if isinstance(f.geometry, adsk.core.Plane) and f.geometry.normal.z > 0.9                     and f.centroid.z >= band_z and f.area > top_area:\n                top_face, top_area = f, f.area\n    if top_face is None:\n        raise RuntimeError('ETCH_NO_TOP_FACE: no upward planar face in the top 5% of the chip bbox')\n    body = top_face.body\n    sk = root.sketches.add(top_face)\n    center = sk.modelToSketchSpace(top_face.centroid)\n    # Measure the face in SKETCH space (model-space bbox axes can be SWAPPED vs\n    # the sketch axes the text lays out in). Project the model bbox corners.\n    bb = top_face.boundingBox\n    p_min = sk.modelToSketchSpace(bb.minPoint)\n    p_max = sk.modelToSketchSpace(bb.maxPoint)\n    face_w = abs(p_max.x - p_min.x)   # extent along sketch-x (the default text baseline)\n    face_h = abs(p_max.y - p_min.y)   # extent along sketch-y\n    # ALWAYS run the text along the LONGEST axis of the top face (most room for\n    # the marking - John's rule, 2026-07-04). If the long axis is sketch-y,\n    # rotate the text 90deg rather than squeezing it onto the short axis.\n    import math as _math\n    rot_deg = 90 if face_h > face_w else 0\n    long_len = max(face_w, face_h)\n    short_len = min(face_w, face_h)\n    w = long_len * 0.80               # 10% margin each side along the long axis\n    box_h_cap = short_len * 0.80      # 10% margin each side across it\n    # multi-line support (e.g. MPN + variant on line 2): fit per-line\n    lines = 'LM358\\nADOM-A'.split(chr(10))\n    n_lines = max(1, len(lines))\n    max_chars = max(1, max(len(l) for l in lines))\n    # width-aware auto-fit: glyph ~0.75*h wide; line block ~1.35*h per line\n    h = (0.5 / 10.0) if 0.5 else         max(0.015, min(box_h_cap / (1.35 * n_lines), w / (0.75 * max_chars)))\n    if rot_deg:\n        c1 = adsk.core.Point3D.create(center.x - box_h_cap/2, center.y - w/2, 0)\n        c2 = adsk.core.Point3D.create(center.x + box_h_cap/2, center.y + w/2, 0)\n    else:\n        c1 = adsk.core.Point3D.create(center.x - w/2, center.y - box_h_cap/2, 0)\n        c2 = adsk.core.Point3D.create(center.x + w/2, center.y + box_h_cap/2, 0)\n    ti = sk.sketchTexts.createInput2('LM358\\nADOM-A', h)\n    ti.setAsMultiLine(c1, c2,\n                      adsk.core.HorizontalAlignments.CenterHorizontalAlignment,\n                      adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)\n    if rot_deg:\n        try:\n            ti.angle = _math.pi / 2.0\n        except Exception:\n            rot_deg = 0  # older API without angle - fall back to unrotated\n    txt = sk.sketchTexts.add(ti)\n    # MEASURE-AND-CORRECT (the margin is a CONTRACT): font metrics vary, so the\n    # 0.75h glyph estimate can under-shoot ('ADOM-A' rendered edge-to-edge). After\n    # placing, measure the text's real bbox; if it busts the 10%-margin box,\n    # recreate at the scaled-down height. Never trust an estimate you can measure.\n    fit_iterations = 0\n    measured_w = None\n    for _pass in range(2):\n        tb = txt.boundingBox\n        t_ext_x = abs(tb.maxPoint.x - tb.minPoint.x)\n        t_ext_y = abs(tb.maxPoint.y - tb.minPoint.y)\n        measured_w = max(t_ext_x, t_ext_y)   # the baseline-axis extent\n        measured_c = min(t_ext_x, t_ext_y)   # the cross-axis extent (line stack)\n        if measured_w <= w and measured_c <= box_h_cap:\n            break\n        scale = min(w / max(measured_w, 1e-6), box_h_cap / max(measured_c, 1e-6)) * 0.95\n        h = max(0.008, h * scale)\n        txt.deleteMe()\n        ti = sk.sketchTexts.createInput2('LM358\\nADOM-A', h)\n        ti.setAsMultiLine(c1, c2,\n                          adsk.core.HorizontalAlignments.CenterHorizontalAlignment,\n                          adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)\n        if rot_deg:\n            try:\n                ti.angle = _math.pi / 2.0\n            except Exception:\n                pass\n        txt = sk.sketchTexts.add(ti)\n        fit_iterations += 1\n    marking_geom = {\n        'chipBBox': {'min': [round(gmin,5), 0, 0], 'maxZ': round(gmax,5)},\n        'chipBBoxZ': {'min': round(gmin,5), 'max': round(gmax,5)},\n        'topBandZ': round(band_z,5),\n        'topFace': {'area': round(top_area,6), 'centroidZ': round(top_face.centroid.z,5),\n                    'sketchExtents': {'x': round(face_w,5), 'y': round(face_h,5)},\n                    'body': body.name},\n        'longAxis': 'sketch-y' if face_h > face_w else 'sketch-x',\n        'rotatedDeg': rot_deg,\n        'textBox': {'c1': [round(c1.x,5), round(c1.y,5)], 'c2': [round(c2.x,5), round(c2.y,5)],\n                    'marginPct': 10},\n        'textHeightMm': round(h*10,4), 'lines': n_lines, 'maxLineChars': max_chars,\n        'heightAuto': not bool(0.5),\n        'textWidthMeasuredMm': round(measured_w*10,4) if measured_w else None,\n        'fitIterations': fit_iterations,\n        'why': {'band': 'top 5% of whole-chip bbox excludes terminal tops that tie the body',\n                'largestArea': 'molded body top beats small terminal faces in the band',\n                'longAxis': 'text runs along the longest face axis for maximum room',\n                'fit': 'h = min(boxH/(1.35*lines), boxW/(0.75*maxChars)) - glyphs ~0.75h wide'},\n    }\n    if 'engraved' == 'engraved':\n        # engraved (sunken) cut - the pre-2026-07-04 default\n        ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.CutFeatureOperation)\n        ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(-0.005))\n        ext_in.participantBodies = [body]\n        root.features.extrudeFeatures.add(ext_in)\n    else:\n        # RAISED WHITE marking (default): humans need CONTRAST - a thin positive\n        # extrude painted white on the dark body reads like real silkscreen ink,\n        # far better than a same-color emboss. Reuses EPG's own rgb-appearance\n        # utility so the white survives Fusion rendering (+ colored STEP).\n        ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)\n        ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(0.005))\n        mark = root.features.extrudeFeatures.add(ext_in)\n        from ElectronicsPackageGenerator.Utilities import addin_utility as _au\n        for i in range(mark.bodies.count):\n            mb = mark.bodies.item(i)\n            mb.name = 'Marking' if i == 0 else 'Marking' + str(i + 1)\n            _au.apply_rgb_appearance(app, design, mb, 255, 255, 255, 'AdomMarkingWhite')\n    etched = 'LM358\\nADOM-A'\n\nstep_path = None\nif '':\n    # Fusion's STEP export writes nothing into a folder that does not exist: make it first\n    os.makedirs(os.path.dirname(os.path.abspath('')), exist_ok=True)\n    em = design.exportManager\n    opts = em.createSTEPExportOptions('')\n    em.execute(opts)\n    if not os.path.isfile(''):\n        raise RuntimeError('Fusion did not write the STEP file %s' % '')\n    step_path = ''\n\n# SIDECAR MANIFEST: every setting we picked + WHY + the bboxes we calculated,\n# so a marking refresh (e.g. adding a variant name as a 2nd line) can rework the\n# text without re-deriving anything. Written next to the STEP as <step>.manifest.json.\nmanifest = {\n    'schema': 'adom-fusion-generate-package/1',\n    'bridgeVersion': 'TESTVER',\n    'type': 'soic',\n    'paramsCm': {'A': 0.175, 'A1': 0.01, 'b': 0.040999999999999995, 'D': 0.49000000000000005, 'E': 0.6, 'E1': 0.39, 'e': 0.127, 'L': 0.08, 'DPins': 8},\n    'paramsNote': 'paramsCm are EPG-native cm (mm inputs were /10)',\n    'marking': ({'text': 'LM358\\nADOM-A', 'style': 'engraved',\n                'depthMm': 0.005 * 10, **marking_geom} if etched else None),\n    'bodies': inv,\n    'outputs': {'step': step_path},\n    'refresh': {\n        'howTo': 'To re-mark (e.g. add a variant on line 2): call fusion_generate_package again '\n                 'with the SAME type+paramsCm (unitsCm:true) and the new multi-line etch text '\n                 '(join lines with a newline). The generator is deterministic, so geometry and '\n                 'placement reproduce exactly; this manifest carries the boxes/height the last '\n                 'run chose for comparison.',\n        'example': {'type': 'soic', 'unitsCm': True, 'params': 'paramsCm from this file',\n                    'etch': ('LM358\\nADOM-A' + chr(10) + 'VARIANT') if etched else 'MPN' + chr(10) + 'VARIANT'},\n    },\n}\nmanifest_path = None\nif step_path:\n    manifest_path = step_path + '.manifest.json'\n    import json as _json\n    with open(manifest_path, 'w', encoding='utf-8') as _f:\n        _json.dump(manifest, _f, indent=2)\n\nvp = app.activeViewport\ncam = vp.camera\ncam.isSmoothTransition = False\ncam.target = adsk.core.Point3D.create(0, 0, 0)\ncam.eye = adsk.core.Point3D.create(0.25, -0.35, 0.45)\ncam.upVector = adsk.core.Vector3D.create(0, 0, 1)\ncam.isFitView = True\nvp.camera = cam\nvp.refresh()\n\nresult = {'type': 'soic', 'bodies': inv, 'etched': etched, 'stepPath': step_path,\n           'manifest': manifest, 'manifestPath': manifest_path,\n           'epgDir': epg_dir, 'doc': app.activeDocument.name}\n",
  "errorCode": null
 },
 {
  "args": {
   "type": "qfn",
   "params": {
    "D": 0.5,
    "E": 0.5,
    "DPins": 4,
    "EPins": 4,
    "color_r": 30
   },
   "unitsCm": true,
   "etch": "it's \"q\"",
   "etchHeightMm": 1
  },
  "script": "\nimport sys, glob, os, importlib\nimport adsk.core, adsk.fusion\n\n# Locate EPG across install conventions + versions (NEVER hardcode the webdeploy hash).\n_bases = []\nfor env in ('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432', 'LOCALAPPDATA'):\n    root = os.environ.get(env)\n    if root:\n        _bases += glob.glob(os.path.join(root, 'Autodesk', 'webdeploy', 'production', '*',\n                                         'Api', 'InternalAddins', 'ElectronicsPackageGenerator'))\nif not _bases:\n    raise RuntimeError('EPG_NOT_FOUND: ElectronicsPackageGenerator not present in this Fusion install')\nepg_dir = max(_bases, key=os.path.getmtime)\nparent = os.path.dirname(epg_dir)\nif parent not in sys.path:\n    sys.path.insert(0, parent)\n\nmod = importlib.import_module('ElectronicsPackageGenerator.Scripts3d.qfn')\n\ndoc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)\ndesign = adsk.fusion.Design.cast(app.activeProduct)\nmod.runWithInput({'D': 0.5, 'E': 0.5, 'DPins': 4, 'EPins': 4, 'color_r': 30}, design)\nroot = design.rootComponent\n\ninv = [{'name': b.name, 'vol': round(b.volume, 6)} for b in root.bRepBodies]\netched = None\nif 'it\\'s \"q\"':\n    # FIND THE TOP SURFACE the robust way (John's algorithm, 2026-07-04):\n    # 1. bounding box of the WHOLE chip (all bodies);\n    # 2. only faces whose centroid sits in the TOP 5% band of that bbox count;\n    # 3. among those upward planar faces take the LARGEST AREA (the molded body\n    #    top beats terminal tops that tie it on height);\n    # 4. lay the text out with a 10% margin inside that face.\n    gmin, gmax = 1e9, -1e9\n    for b in root.bRepBodies:\n        bb = b.boundingBox\n        gmin = min(gmin, bb.minPoint.z)\n        gmax = max(gmax, bb.maxPoint.z)\n    band_z = gmax - 0.05 * (gmax - gmin)\n    top_face, top_area = None, -1.0\n    for b in root.bRepBodies:\n        for f in b.faces:\n            if isinstance(f.geometry, adsk.core.Plane) and f.geometry.normal.z > 0.9                     and f.centroid.z >= band_z and f.area > top_area:\n                top_face, top_area = f, f.area\n    if top_face is None:\n        raise RuntimeError('ETCH_NO_TOP_FACE: no upward planar face in the top 5% of the chip bbox')\n    body = top_face.body\n    sk = root.sketches.add(top_face)\n    center = sk.modelToSketchSpace(top_face.centroid)\n    # Measure the face in SKETCH space (model-space bbox axes can be SWAPPED vs\n    # the sketch axes the text lays out in). Project the model bbox corners.\n    bb = top_face.boundingBox\n    p_min = sk.modelToSketchSpace(bb.minPoint)\n    p_max = sk.modelToSketchSpace(bb.maxPoint)\n    face_w = abs(p_max.x - p_min.x)   # extent along sketch-x (the default text baseline)\n    face_h = abs(p_max.y - p_min.y)   # extent along sketch-y\n    # ALWAYS run the text along the LONGEST axis of the top face (most room for\n    # the marking - John's rule, 2026-07-04). If the long axis is sketch-y,\n    # rotate the text 90deg rather than squeezing it onto the short axis.\n    import math as _math\n    rot_deg = 90 if face_h > face_w else 0\n    long_len = max(face_w, face_h)\n    short_len = min(face_w, face_h)\n    w = long_len * 0.80               # 10% margin each side along the long axis\n    box_h_cap = short_len * 0.80      # 10% margin each side across it\n    # multi-line support (e.g. MPN + variant on line 2): fit per-line\n    lines = 'it\\'s \"q\"'.split(chr(10))\n    n_lines = max(1, len(lines))\n    max_chars = max(1, max(len(l) for l in lines))\n    # width-aware auto-fit: glyph ~0.75*h wide; line block ~1.35*h per line\n    h = (1 / 10.0) if 1 else         max(0.015, min(box_h_cap / (1.35 * n_lines), w / (0.75 * max_chars)))\n    if rot_deg:\n        c1 = adsk.core.Point3D.create(center.x - box_h_cap/2, center.y - w/2, 0)\n        c2 = adsk.core.Point3D.create(center.x + box_h_cap/2, center.y + w/2, 0)\n    else:\n        c1 = adsk.core.Point3D.create(center.x - w/2, center.y - box_h_cap/2, 0)\n        c2 = adsk.core.Point3D.create(center.x + w/2, center.y + box_h_cap/2, 0)\n    ti = sk.sketchTexts.createInput2('it\\'s \"q\"', h)\n    ti.setAsMultiLine(c1, c2,\n                      adsk.core.HorizontalAlignments.CenterHorizontalAlignment,\n                      adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)\n    if rot_deg:\n        try:\n            ti.angle = _math.pi / 2.0\n        except Exception:\n            rot_deg = 0  # older API without angle - fall back to unrotated\n    txt = sk.sketchTexts.add(ti)\n    # MEASURE-AND-CORRECT (the margin is a CONTRACT): font metrics vary, so the\n    # 0.75h glyph estimate can under-shoot ('ADOM-A' rendered edge-to-edge). After\n    # placing, measure the text's real bbox; if it busts the 10%-margin box,\n    # recreate at the scaled-down height. Never trust an estimate you can measure.\n    fit_iterations = 0\n    measured_w = None\n    for _pass in range(2):\n        tb = txt.boundingBox\n        t_ext_x = abs(tb.maxPoint.x - tb.minPoint.x)\n        t_ext_y = abs(tb.maxPoint.y - tb.minPoint.y)\n        measured_w = max(t_ext_x, t_ext_y)   # the baseline-axis extent\n        measured_c = min(t_ext_x, t_ext_y)   # the cross-axis extent (line stack)\n        if measured_w <= w and measured_c <= box_h_cap:\n            break\n        scale = min(w / max(measured_w, 1e-6), box_h_cap / max(measured_c, 1e-6)) * 0.95\n        h = max(0.008, h * scale)\n        txt.deleteMe()\n        ti = sk.sketchTexts.createInput2('it\\'s \"q\"', h)\n        ti.setAsMultiLine(c1, c2,\n                          adsk.core.HorizontalAlignments.CenterHorizontalAlignment,\n                          adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)\n        if rot_deg:\n            try:\n                ti.angle = _math.pi / 2.0\n            except Exception:\n                pass\n        txt = sk.sketchTexts.add(ti)\n        fit_iterations += 1\n    marking_geom = {\n        'chipBBox': {'min': [round(gmin,5), 0, 0], 'maxZ': round(gmax,5)},\n        'chipBBoxZ': {'min': round(gmin,5), 'max': round(gmax,5)},\n        'topBandZ': round(band_z,5),\n        'topFace': {'area': round(top_area,6), 'centroidZ': round(top_face.centroid.z,5),\n                    'sketchExtents': {'x': round(face_w,5), 'y': round(face_h,5)},\n                    'body': body.name},\n        'longAxis': 'sketch-y' if face_h > face_w else 'sketch-x',\n        'rotatedDeg': rot_deg,\n        'textBox': {'c1': [round(c1.x,5), round(c1.y,5)], 'c2': [round(c2.x,5), round(c2.y,5)],\n                    'marginPct': 10},\n        'textHeightMm': round(h*10,4), 'lines': n_lines, 'maxLineChars': max_chars,\n        'heightAuto': not bool(1),\n        'textWidthMeasuredMm': round(measured_w*10,4) if measured_w else None,\n        'fitIterations': fit_iterations,\n        'why': {'band': 'top 5% of whole-chip bbox excludes terminal tops that tie the body',\n                'largestArea': 'molded body top beats small terminal faces in the band',\n                'longAxis': 'text runs along the longest face axis for maximum room',\n                'fit': 'h = min(boxH/(1.35*lines), boxW/(0.75*maxChars)) - glyphs ~0.75h wide'},\n    }\n    if 'raised' == 'engraved':\n        # engraved (sunken) cut - the pre-2026-07-04 default\n        ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.CutFeatureOperation)\n        ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(-0.003))\n        ext_in.participantBodies = [body]\n        root.features.extrudeFeatures.add(ext_in)\n    else:\n        # RAISED WHITE marking (default): humans need CONTRAST - a thin positive\n        # extrude painted white on the dark body reads like real silkscreen ink,\n        # far better than a same-color emboss. Reuses EPG's own rgb-appearance\n        # utility so the white survives Fusion rendering (+ colored STEP).\n        ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)\n        ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(0.003))\n        mark = root.features.extrudeFeatures.add(ext_in)\n        from ElectronicsPackageGenerator.Utilities import addin_utility as _au\n        for i in range(mark.bodies.count):\n            mb = mark.bodies.item(i)\n            mb.name = 'Marking' if i == 0 else 'Marking' + str(i + 1)\n            _au.apply_rgb_appearance(app, design, mb, 255, 255, 255, 'AdomMarkingWhite')\n    etched = 'it\\'s \"q\"'\n\nstep_path = None\nif '':\n    # Fusion's STEP export writes nothing into a folder that does not exist: make it first\n    os.makedirs(os.path.dirname(os.path.abspath('')), exist_ok=True)\n    em = design.exportManager\n    opts = em.createSTEPExportOptions('')\n    em.execute(opts)\n    if not os.path.isfile(''):\n        raise RuntimeError('Fusion did not write the STEP file %s' % '')\n    step_path = ''\n\n# SIDECAR MANIFEST: every setting we picked + WHY + the bboxes we calculated,\n# so a marking refresh (e.g. adding a variant name as a 2nd line) can rework the\n# text without re-deriving anything. Written next to the STEP as <step>.manifest.json.\nmanifest = {\n    'schema': 'adom-fusion-generate-package/1',\n    'bridgeVersion': 'TESTVER',\n    'type': 'qfn',\n    'paramsCm': {'D': 0.5, 'E': 0.5, 'DPins': 4, 'EPins': 4, 'color_r': 30},\n    'paramsNote': 'paramsCm are EPG-native cm (mm inputs were /10)',\n    'marking': ({'text': 'it\\'s \"q\"', 'style': 'raised',\n                'depthMm': 0.003 * 10, **marking_geom} if etched else None),\n    'bodies': inv,\n    'outputs': {'step': step_path},\n    'refresh': {\n        'howTo': 'To re-mark (e.g. add a variant on line 2): call fusion_generate_package again '\n                 'with the SAME type+paramsCm (unitsCm:true) and the new multi-line etch text '\n                 '(join lines with a newline). The generator is deterministic, so geometry and '\n                 'placement reproduce exactly; this manifest carries the boxes/height the last '\n                 'run chose for comparison.',\n        'example': {'type': 'qfn', 'unitsCm': True, 'params': 'paramsCm from this file',\n                    'etch': ('it\\'s \"q\"' + chr(10) + 'VARIANT') if etched else 'MPN' + chr(10) + 'VARIANT'},\n    },\n}\nmanifest_path = None\nif step_path:\n    manifest_path = step_path + '.manifest.json'\n    import json as _json\n    with open(manifest_path, 'w', encoding='utf-8') as _f:\n        _json.dump(manifest, _f, indent=2)\n\nvp = app.activeViewport\ncam = vp.camera\ncam.isSmoothTransition = False\ncam.target = adsk.core.Point3D.create(0, 0, 0)\ncam.eye = adsk.core.Point3D.create(0.25, -0.35, 0.45)\ncam.upVector = adsk.core.Vector3D.create(0, 0, 1)\ncam.isFitView = True\nvp.camera = cam\nvp.refresh()\n\nresult = {'type': 'qfn', 'bodies': inv, 'etched': etched, 'stepPath': step_path,\n           'manifest': manifest, 'manifestPath': manifest_path,\n           'epgDir': epg_dir, 'doc': app.activeDocument.name}\n",
  "errorCode": null
 },
 {
  "args": {
   "type": "chip",
   "params": {
    "D": 3,
    "E": 1e-05,
    "A": 1.2345678901234568e+16
   },
   "etch": ""
  },
  "script": "\nimport sys, glob, os, importlib\nimport adsk.core, adsk.fusion\n\n# Locate EPG across install conventions + versions (NEVER hardcode the webdeploy hash).\n_bases = []\nfor env in ('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432', 'LOCALAPPDATA'):\n    root = os.environ.get(env)\n    if root:\n        _bases += glob.glob(os.path.join(root, 'Autodesk', 'webdeploy', 'production', '*',\n                                         'Api', 'InternalAddins', 'ElectronicsPackageGenerator'))\nif not _bases:\n    raise RuntimeError('EPG_NOT_FOUND: ElectronicsPackageGenerator not present in this Fusion install')\nepg_dir = max(_bases, key=os.path.getmtime)\nparent = os.path.dirname(epg_dir)\nif parent not in sys.path:\n    sys.path.insert(0, parent)\n\nmod = importlib.import_module('ElectronicsPackageGenerator.Scripts3d.chip')\n\ndoc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)\ndesign = adsk.fusion.Design.cast(app.activeProduct)\nmod.runWithInput({'D': 0.3, 'E': 1.0000000000000002e-06, 'A': 1234567890123456.8}, design)\nroot = design.rootComponent\n\ninv = [{'name': b.name, 'vol': round(b.volume, 6)} for b in root.bRepBodies]\netched = None\nif '':\n    # FIND THE TOP SURFACE the robust way (John's algorithm, 2026-07-04):\n    # 1. bounding box of the WHOLE chip (all bodies);\n    # 2. only faces whose centroid sits in the TOP 5% band of that bbox count;\n    # 3. among those upward planar faces take the LARGEST AREA (the molded body\n    #    top beats terminal tops that tie it on height);\n    # 4. lay the text out with a 10% margin inside that face.\n    gmin, gmax = 1e9, -1e9\n    for b in root.bRepBodies:\n        bb = b.boundingBox\n        gmin = min(gmin, bb.minPoint.z)\n        gmax = max(gmax, bb.maxPoint.z)\n    band_z = gmax - 0.05 * (gmax - gmin)\n    top_face, top_area = None, -1.0\n    for b in root.bRepBodies:\n        for f in b.faces:\n            if isinstance(f.geometry, adsk.core.Plane) and f.geometry.normal.z > 0.9                     and f.centroid.z >= band_z and f.area > top_area:\n                top_face, top_area = f, f.area\n    if top_face is None:\n        raise RuntimeError('ETCH_NO_TOP_FACE: no upward planar face in the top 5% of the chip bbox')\n    body = top_face.body\n    sk = root.sketches.add(top_face)\n    center = sk.modelToSketchSpace(top_face.centroid)\n    # Measure the face in SKETCH space (model-space bbox axes can be SWAPPED vs\n    # the sketch axes the text lays out in). Project the model bbox corners.\n    bb = top_face.boundingBox\n    p_min = sk.modelToSketchSpace(bb.minPoint)\n    p_max = sk.modelToSketchSpace(bb.maxPoint)\n    face_w = abs(p_max.x - p_min.x)   # extent along sketch-x (the default text baseline)\n    face_h = abs(p_max.y - p_min.y)   # extent along sketch-y\n    # ALWAYS run the text along the LONGEST axis of the top face (most room for\n    # the marking - John's rule, 2026-07-04). If the long axis is sketch-y,\n    # rotate the text 90deg rather than squeezing it onto the short axis.\n    import math as _math\n    rot_deg = 90 if face_h > face_w else 0\n    long_len = max(face_w, face_h)\n    short_len = min(face_w, face_h)\n    w = long_len * 0.80               # 10% margin each side along the long axis\n    box_h_cap = short_len * 0.80      # 10% margin each side across it\n    # multi-line support (e.g. MPN + variant on line 2): fit per-line\n    lines = ''.split(chr(10))\n    n_lines = max(1, len(lines))\n    max_chars = max(1, max(len(l) for l in lines))\n    # width-aware auto-fit: glyph ~0.75*h wide; line block ~1.35*h per line\n    h = (None / 10.0) if None else         max(0.015, min(box_h_cap / (1.35 * n_lines), w / (0.75 * max_chars)))\n    if rot_deg:\n        c1 = adsk.core.Point3D.create(center.x - box_h_cap/2, center.y - w/2, 0)\n        c2 = adsk.core.Point3D.create(center.x + box_h_cap/2, center.y + w/2, 0)\n    else:\n        c1 = adsk.core.Point3D.create(center.x - w/2, center.y - box_h_cap/2, 0)\n        c2 = adsk.core.Point3D.create(center.x + w/2, center.y + box_h_cap/2, 0)\n    ti = sk.sketchTexts.createInput2('', h)\n    ti.setAsMultiLine(c1, c2,\n                      adsk.core.HorizontalAlignments.CenterHorizontalAlignment,\n                      adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)\n    if rot_deg:\n        try:\n            ti.angle = _math.pi / 2.0\n        except Exception:\n            rot_deg = 0  # older API without angle - fall back to unrotated\n    txt = sk.sketchTexts.add(ti)\n    # MEASURE-AND-CORRECT (the margin is a CONTRACT): font metrics vary, so the\n    # 0.75h glyph estimate can under-shoot ('ADOM-A' rendered edge-to-edge). After\n    # placing, measure the text's real bbox; if it busts the 10%-margin box,\n    # recreate at the scaled-down height. Never trust an estimate you can measure.\n    fit_iterations = 0\n    measured_w = None\n    for _pass in range(2):\n        tb = txt.boundingBox\n        t_ext_x = abs(tb.maxPoint.x - tb.minPoint.x)\n        t_ext_y = abs(tb.maxPoint.y - tb.minPoint.y)\n        measured_w = max(t_ext_x, t_ext_y)   # the baseline-axis extent\n        measured_c = min(t_ext_x, t_ext_y)   # the cross-axis extent (line stack)\n        if measured_w <= w and measured_c <= box_h_cap:\n            break\n        scale = min(w / max(measured_w, 1e-6), box_h_cap / max(measured_c, 1e-6)) * 0.95\n        h = max(0.008, h * scale)\n        txt.deleteMe()\n        ti = sk.sketchTexts.createInput2('', h)\n        ti.setAsMultiLine(c1, c2,\n                          adsk.core.HorizontalAlignments.CenterHorizontalAlignment,\n                          adsk.core.VerticalAlignments.MiddleVerticalAlignment, 0)\n        if rot_deg:\n            try:\n                ti.angle = _math.pi / 2.0\n            except Exception:\n                pass\n        txt = sk.sketchTexts.add(ti)\n        fit_iterations += 1\n    marking_geom = {\n        'chipBBox': {'min': [round(gmin,5), 0, 0], 'maxZ': round(gmax,5)},\n        'chipBBoxZ': {'min': round(gmin,5), 'max': round(gmax,5)},\n        'topBandZ': round(band_z,5),\n        'topFace': {'area': round(top_area,6), 'centroidZ': round(top_face.centroid.z,5),\n                    'sketchExtents': {'x': round(face_w,5), 'y': round(face_h,5)},\n                    'body': body.name},\n        'longAxis': 'sketch-y' if face_h > face_w else 'sketch-x',\n        'rotatedDeg': rot_deg,\n        'textBox': {'c1': [round(c1.x,5), round(c1.y,5)], 'c2': [round(c2.x,5), round(c2.y,5)],\n                    'marginPct': 10},\n        'textHeightMm': round(h*10,4), 'lines': n_lines, 'maxLineChars': max_chars,\n        'heightAuto': not bool(None),\n        'textWidthMeasuredMm': round(measured_w*10,4) if measured_w else None,\n        'fitIterations': fit_iterations,\n        'why': {'band': 'top 5% of whole-chip bbox excludes terminal tops that tie the body',\n                'largestArea': 'molded body top beats small terminal faces in the band',\n                'longAxis': 'text runs along the longest face axis for maximum room',\n                'fit': 'h = min(boxH/(1.35*lines), boxW/(0.75*maxChars)) - glyphs ~0.75h wide'},\n    }\n    if 'raised' == 'engraved':\n        # engraved (sunken) cut - the pre-2026-07-04 default\n        ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.CutFeatureOperation)\n        ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(-0.003))\n        ext_in.participantBodies = [body]\n        root.features.extrudeFeatures.add(ext_in)\n    else:\n        # RAISED WHITE marking (default): humans need CONTRAST - a thin positive\n        # extrude painted white on the dark body reads like real silkscreen ink,\n        # far better than a same-color emboss. Reuses EPG's own rgb-appearance\n        # utility so the white survives Fusion rendering (+ colored STEP).\n        ext_in = root.features.extrudeFeatures.createInput(txt, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)\n        ext_in.setDistanceExtent(False, adsk.core.ValueInput.createByReal(0.003))\n        mark = root.features.extrudeFeatures.add(ext_in)\n        from ElectronicsPackageGenerator.Utilities import addin_utility as _au\n        for i in range(mark.bodies.count):\n            mb = mark.bodies.item(i)\n            mb.name = 'Marking' if i == 0 else 'Marking' + str(i + 1)\n            _au.apply_rgb_appearance(app, design, mb, 255, 255, 255, 'AdomMarkingWhite')\n    etched = ''\n\nstep_path = None\nif '':\n    # Fusion's STEP export writes nothing into a folder that does not exist: make it first\n    os.makedirs(os.path.dirname(os.path.abspath('')), exist_ok=True)\n    em = design.exportManager\n    opts = em.createSTEPExportOptions('')\n    em.execute(opts)\n    if not os.path.isfile(''):\n        raise RuntimeError('Fusion did not write the STEP file %s' % '')\n    step_path = ''\n\n# SIDECAR MANIFEST: every setting we picked + WHY + the bboxes we calculated,\n# so a marking refresh (e.g. adding a variant name as a 2nd line) can rework the\n# text without re-deriving anything. Written next to the STEP as <step>.manifest.json.\nmanifest = {\n    'schema': 'adom-fusion-generate-package/1',\n    'bridgeVersion': 'TESTVER',\n    'type': 'chip',\n    'paramsCm': {'D': 0.3, 'E': 1.0000000000000002e-06, 'A': 1234567890123456.8},\n    'paramsNote': 'paramsCm are EPG-native cm (mm inputs were /10)',\n    'marking': ({'text': '', 'style': 'raised',\n                'depthMm': 0.003 * 10, **marking_geom} if etched else None),\n    'bodies': inv,\n    'outputs': {'step': step_path},\n    'refresh': {\n        'howTo': 'To re-mark (e.g. add a variant on line 2): call fusion_generate_package again '\n                 'with the SAME type+paramsCm (unitsCm:true) and the new multi-line etch text '\n                 '(join lines with a newline). The generator is deterministic, so geometry and '\n                 'placement reproduce exactly; this manifest carries the boxes/height the last '\n                 'run chose for comparison.',\n        'example': {'type': 'chip', 'unitsCm': True, 'params': 'paramsCm from this file',\n                    'etch': ('' + chr(10) + 'VARIANT') if etched else 'MPN' + chr(10) + 'VARIANT'},\n    },\n}\nmanifest_path = None\nif step_path:\n    manifest_path = step_path + '.manifest.json'\n    import json as _json\n    with open(manifest_path, 'w', encoding='utf-8') as _f:\n        _json.dump(manifest, _f, indent=2)\n\nvp = app.activeViewport\ncam = vp.camera\ncam.isSmoothTransition = False\ncam.target = adsk.core.Point3D.create(0, 0, 0)\ncam.eye = adsk.core.Point3D.create(0.25, -0.35, 0.45)\ncam.upVector = adsk.core.Vector3D.create(0, 0, 1)\ncam.isFitView = True\nvp.camera = cam\nvp.refresh()\n\nresult = {'type': 'chip', 'bodies': inv, 'etched': etched, 'stepPath': step_path,\n           'manifest': manifest, 'manifestPath': manifest_path,\n           'epgDir': epg_dir, 'doc': app.activeDocument.name}\n",
  "errorCode": null
 },
 {
  "args": {
   "type": "bogus",
   "params": {}
  },
  "script": null,
  "errorCode": "unknown_package_type"
 }
]