← Commit history

v1.6.74: fusion_board_stackup verb + pcb-stackup skill (physical fab stack, exploded view, z-fighting, publish-everything) + AI hints

John Lauer ·fc67911062 ·3mo ago ·parent 9e75576
7 files changed +246−8
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.73\ No newline at end of file+1.6.74\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.73",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.73/adom-bridge-fusion-v1.6.73.zip",-  "sha256": "720325557ad11533caf7b618588255533b37fcff50924051be229d6d9862a4a2",-  "size": 373207,+  "version": "1.6.74",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.74/adom-bridge-fusion-v1.6.74.zip",+  "sha256": "d4fded77e6f08362c413cb6acc359a5e827dbb6d0adaf447897b4cf57dd640a2",+  "size": 379830,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.73",+  "version": "1.6.74",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop-fusion-bridge",   "author": "Adom Inc.",
describe.py+2−1
@@ -132,8 +132,9 @@ _T = [     ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}),     ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}),     ("fusion_export_usdz", "Export to USDZ (AR / Hydrogen 3D).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.usdz"}),-    ("fusion_export_optimized_glb", "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}),+    ("fusion_export_optimized_glb", "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d. STACKUP RULE (John 2026-07-15, see pcb-stackup skill): board bbox = the FR4 body, NEVER the whole assembly (components overhang edges + tower above); each overlay goes at ITS layer's z (copper at FR4-top, silk above mask); copper traces are REAL 35um 3D bodies colored like the FR4 - recolor them + translucent mask in the RAW GLB before optimizing to make routing visible; put the measured stackup table on the board's wiki page. PUBLISH EVERYTHING: a board's wiki page repo must carry EVERY exportable design file (.f3d/.step/.usdz/EAGLE .brd+.sch/Fusion .fbrd+.fsch/gerbers/BOM/CPL/GLBs/renders), not just the 3D - the page exists to share the design.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}),     ("fusion_fetch_optimized_glb", "Finish a fusion_export_optimized_glb job: bounded-poll the optimizer by jobId and write the GLB to outputPath when complete. Re-call until status=complete (big boards tessellate a few minutes). No Fusion needed.", {"jobId": "required str", "outputPath": "str (.glb)", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "status": "str", "pending": "bool", "sizeBytes": "int"}, 150, None, False, {"jobId": "abc123", "outputPath": "C:/out/board.glb"}),+    ("fusion_board_stackup", "Read a board's PHYSICAL fabrication stackup (see the pcb-stackup skill): copper layer count + copper/dielectric thicknesses from the EAGLE design rules (layerSetup/mtCopper/mtIsolate) plus the measured FR4 extent. A PCB is Cu/prepreg/Cu/core/.../Cu - the FR4 is NOT one slab. Use to build the stackup table + real-thickness exploded 3D view, and put a stackup table on every exported board's wiki page.", {}, {"layerSetup": "str", "copperLayers": "list", "copperCount": "int", "copperThickness_mm": "list", "dielectricBonds": "list (prepreg/core order)", "fr4": "{x_mm,y_mm,dielectric_mm}"}, 200, None, True, {}),     ("fusion_export_obj", "Export to OBJ mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.obj"}),     ("fusion_export_dxf", "Export 2D DXF.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dxf"}),     ("fusion_export_dwg", "Export 2D DWG.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dwg"}),
server.py+76−1
@@ -3482,6 +3482,79 @@ def _orchestrate_fetch_optimized_glb(args: dict) -> dict:             "message": "Job %s still %s; re-call fusion_fetch_optimized_glb to finish." % (job_id, res.get("status") or "processing")}  +def _orchestrate_board_stackup(args: dict) -> dict:+    """Read a board's PHYSICAL fabrication stackup (see the pcb-stackup skill). A PCB is a+    stack of copper + dielectric: Cu(L1) / prepreg / Cu(L2) / core / ... / Cu(bottom). This+    reads the copper count + copper/dielectric thicknesses from the EAGLE design rules+    (layerSetup / mtCopper / mtIsolate in the .brd) and the measured FR4 extent from the 3D+    body, and returns the ordered layer list (name, thickness, z) so a caller can build the+    stackup table + the real-thickness exploded 3D view."""+    import re as _re+    import tempfile as _tf+    # 1) design rules from the EAGLE .brd (board editor active)+    _proxy_to_addin("show_2d_board", {}, timeout=60)+    brd = os.path.join(_tf.gettempdir(), "adom_stackup.brd")+    r = _proxy_to_addin("export_eagle_source", {"outputPath": brd}, timeout=120)+    if not r.get("success"):+        return {"success": False, "error": "could not export .brd for stackup: %s" % (r.get("error") or r.get("message")),+                "_hint": "Open the PROJECT and switch to the board (fusion_show_2d_board) first."}+    try:+        txt = open(brd, encoding="latin1", errors="replace").read()+    except Exception as e:+        return {"success": False, "error": "could not read .brd: %s" % e}+    def _p(name):+        m = _re.search(r'<param name="%s" value="([^"]*)"' % name, txt)+        return m.group(1) if m else None+    layer_setup = _p("layerSetup") or ""+    mt_copper = [x for x in (_p("mtCopper") or "").split()]+    mt_isolate = [x for x in (_p("mtIsolate") or "").split()]+    copper_layers = _re.findall(r"\d+", layer_setup)   # e.g. ['1','2','15','16']+    ncu = len(copper_layers)+    # bonds between copper layers, in order: '+' prepreg, '*' core+    bonds = [("core" if c == "*" else "prepreg") for c in layer_setup if c in "+*"]+    # 2) measure the FR4 Board body (3D)+    _proxy_to_addin("show_3d_board", {}, timeout=60)+    mscript = (+        "import adsk.core, adsk.fusion\n"+        "app=adsk.core.Application.get(); des=adsk.fusion.Design.cast(app.activeProduct)\n"+        "res='none'\n"+        "for occ in des.rootComponent.allOccurrences:\n"+        " for b in occ.component.bRepBodies:\n"+        "  if b.name.lower()=='board':\n"+        "   bb=b.boundingBox; mn=bb.minPoint; mx=bb.maxPoint\n"+        "   res='%.4f %.4f %.4f'%((mx.x-mn.x)*10,(mx.y-mn.y)*10,(mx.z-mn.z)*10)\n"+        "print('FR4 '+res)")+    mm = _proxy_to_addin("run_modeling_script", {"script": mscript}, timeout=60)+    fr4 = None+    try:+        msg = ""+        if isinstance(mm, dict):+            o = mm.get("output")+            msg = (json.loads(o).get("message") if isinstance(o, str) and o.startswith("{") else (mm.get("message") or o)) or ""+        m = _re.search(r"FR4\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)", str(msg))+        if m:+            fr4 = {"x_mm": float(m.group(1)), "y_mm": float(m.group(2)), "dielectric_mm": float(m.group(3))}+    except Exception:+        pass+    def _num(s):+        try: return float(str(s).replace("mm", ""))+        except Exception: return None+    return {+        "success": True,+        "layerSetup": layer_setup,+        "copperLayers": copper_layers,+        "copperCount": ncu,+        "copperThickness_mm": [_num(t) for t in mt_copper[:ncu]] if mt_copper else None,+        "dielectricBonds": bonds,                 # e.g. ['prepreg','core','prepreg']+        "dielectricThickness_raw": mt_isolate,    # design-rule list (may hold 2-layer defaults)+        "fr4": fr4,+        "brdPath": brd,+        "_hint": ("Physical stack (see pcb-stackup skill): the FR4 is NOT one slab - it is "+                  "prepreg/core/prepreg with copper between. Build the table + exploded view per that skill. "+                  "If mtIsolate looks like 2-layer defaults, fit a symmetric split to fr4.dielectric_mm."),+    }++ def dispatch_command(command: str, args: dict) -> dict:     """Dispatch a command to the appropriate handler."""     # Direct handlers (don't need the add-in)@@ -3494,7 +3567,7 @@ def dispatch_command(command: str, args: dict) -> dict:     # The user should launch Fusion themselves; we just report the status.     # Commands that need Fusion running (add-in commands + orchestrated commands)     _OPEN_WITH_SCREENSHOT = {"open_schematic", "open_board", "show_3d_board", "show_2d_board", "show_schematic", "import_electronics"}-    if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views", "cleanup_cloud_files", "generate_package", "export_optimized_glb") or command in _OPEN_WITH_SCREENSHOT:+    if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views", "cleanup_cloud_files", "generate_package", "export_optimized_glb", "board_stackup") or command in _OPEN_WITH_SCREENSHOT:         if not fusion_info.get("installed"):             return {                 "success": False,@@ -3612,6 +3685,8 @@ def dispatch_command(command: str, args: dict) -> dict:         return _orchestrate_export_optimized_glb(args)     if command == "fetch_optimized_glb":         return _orchestrate_fetch_optimized_glb(args)+    if command == "board_stackup":+        return _orchestrate_board_stackup(args)      # Add-in proxy commands     if command in ADDIN_COMMANDS:
skills/fusion-molecule-export/SKILL.md+22
@@ -123,6 +123,28 @@ preserves the Fusion material colors). `adom-wiki pkg init <slug> --type compone `repo push` the page.json + GLB + images (publish only ships the package layer, NOT the page repo). The native `/viewer/3d/component/<slug>` iframe renders the GLB in Babylon. Verified live. ++## PUBLISH EVERYTHING (John, hard rule, 2026-07-15)++A board/molecule wiki page is about SHARING. Push absolutely every design file you can produce into+the page repo - never just the GLB + images. The full set for a Fusion board:++| File | How |+|---|---|+| `.f3d` (Fusion archive) | `fusion_export_f3d` (3D view active) |+| `.step` | `fusion_export_step` |+| `.usdz` | `fusion_export_usdz` |+| EAGLE `.brd` / `.sch` | `fusion_export_eagle_source` (board / schematic editor active) |+| Fusion `.fbrd` / `.fsch` | `fusion_export_source` (board / schematic editor active) |+| gerbers ZIP | `fusion_export_gerbers` |+| BOM `.csv` | `fusion_export_bom` (board editor) |+| CPL (pick and place) `.csv` | `fusion_export_cpl` (board editor) |+| GLB (assembled + exploded) | the optimized-GLB pipeline in this skill |+| renders (iso/top), schematic PNG | viewport captures |++If a file type exists for the design, it belongs in the repo. List them all in the README Files+section so humans see what is available.+ ## Related - `fusion-electronics` / `fusion-eagle-commands` - open the PROJECT and drive the editors first. - `adom-molecule` (user skill) - the CLI reference (note its auth gap above).
skills/pcb-stackup/SKILL.mdadded+140
@@ -0,0 +1,140 @@+---+name: pcb-stackup+description: >+  Think about a PCB as its STACKUP - the physical layer sandwich - before doing ANY 3D work with a+  board: placing overlays (silkscreen/copper textures), computing the board bounding box, picking+  z-heights, making traces visible, or building a wiki 3D model. Records John's teaching (2026-07-15)+  after an overlay was placed at component height instead of the copper layer. Trigger words: pcb+  stackup, board layers, fr4 bounding box, board bbox wrong, silkscreen z height, copper layer z,+  traces not visible, overlay floating above board, layer heights, soldermask z, board thickness.+---++# PCB stackup - how to think about a board's layers (John, 2026-07-15)++A PCB is not "a green slab with parts". It is a **stack**, and every 3D/overlay/bbox decision must+be made per-layer, at that layer's z-height. From bottom to top (typical 1.6mm board):++```+components (top side)            <- pick-and-place, then solder reflow welds them on+top silkscreen (ink)             <- printed ON the soldermask+top soldermask        ~65 um     <- the green; OPENS over pads+top copper (traces)   ~35 um     <- layer 1; the routing+FR4 core + inner Cu   ~1.5 mm    <- inner layers (e.g. 2, 15) live inside+bottom copper         ~35 um     <- layer 16+bottom soldermask     ~65 um+bottom silkscreen+components (bottom side)+```++Measured live on Adonis's BQ25792 board (from the RAW GLB bodies): FR4 core z 0.000-1.516, top+copper 1.516-1.551, top mask 1.516-1.582, bottom copper -0.035-0.000, bottom mask -0.065-0.000.++## The rules this implies (each one was violated once and looked wrong)++1. **The board bbox = the FR4 body, never the whole assembly.** Components overhang edges (a switch+   hung off the BQ board and inflated Y 26.4 -> 28.2mm) and tower above (a quad placed at+   whole-assembly z-max floated ON TOP of the components). Measure the substrate body itself.+2. **Each overlay goes at ITS layer's z.** Copper texture -> copper layer z (FR4 top + eps).+   Silkscreen -> above the mask top. Never "board bbox top + eps".+3. **Copper traces ARE 3D geometry in a Fusion export** - real bodies, 35um thick, so they are NOT+   heavy. They are invisible for two reasons only: Fusion colors them IDENTICAL to the FR4+   appearance, and the soldermask exports opaque. Fix = recolor the copper bodies to a real copper+   material + make the mask translucent (alpha ~0.6 BLEND). No texture overlay needed.+4. **Measure bodies in the RAW (pre-optimize) GLB.** After join/flatten (gltf-transform optimize or+   the service molecule mode), meshes merge across bodies and per-layer semantics are DESTROYED+   (the "Board" node becomes a 10mm-thick mega-mesh). Identify FR4/copper/mask bodies FIRST (raw GLB+   node names: `Board`, `1-copper`, `16-copper`, `1-soldermask`, `16-soldermask`), retarget their+   materials, THEN optimize - join preserves distinct materials.+5. **The stackup is queryable, don't guess.** Two sources:+   - EAGLE `.brd` design rules (`fusion_export_eagle_source`): `layerSetup` (e.g. `(1+2*15+16)` =+     4-layer), `mtCopper` (copper thicknesses), `mtIsolate` (dielectric thicknesses).+   - The raw exported 3D bodies' z-ranges (as measured above). Fusion also shows a stackup dialog;+     the .brd rules are its programmatic equivalent.+6. **Put the stackup table on the wiki page** of every exported board (John wants this standard):+   a layer/Z/thickness table like the BQ25792 page's "Board stackup" section.++## Recipe: visible REAL traces in an optimized GLB (proven on the BQ board)++1. `fusion_export_step` -> raw STEP -> raw GLB (adom-step2glb convert). Bodies stay distinct.+2. In the raw GLB, retarget the `*-copper` nodes' primitives to a new metallic copper material+   (baseColor ~[0.85,0.42,0.12], metallic 1, roughness 0.35) and the `*-soldermask` nodes to a+   translucent green (alpha ~0.62, BLEND, doubleSided).+3. `gltf-transform optimize --compress draco` -> ~800KB with the copper as its OWN mesh.+   Result: real 3D routing visible under the mask, stackup-correct. 19MB -> 822KB on the BQ board.+++## Z-fighting: coplanar faces are the enemy (learned live, 2026-07-15)++At true exported heights the stack has PERFECTLY coplanar faces: FR4 top = copper bottom = mask+bottom (one plane, e.g. z=1.516). Renderers z-fight (shimmer) on them. Fix: micro z-offsets that+break coplanarity but are invisible at view scale - mask +/-5um away from the board, copper +/-2um.+Apply to the RAW GLB nodes before optimizing (catch nested sub-assembly copies of copper/mask too).++## Inner layers do NOT exist as 3D bodies++A 4-layer board (layerSetup (1+2*15+16)) exports only OUTER copper as bodies (1-copper, 16-copper).+Inner layers (2, 15) live only in the GERBERS (Fusion CAM names them copper_l2.G1, copper_l3.G2).+To show them in 3D: render those gerbers to copper-colored transparent PNGs framed EXACTLY to the+Profile outline, and add them as textured quads at their stackup z (inside the FR4 core).++## Exploded stackup view (a standard option users may ask for)++Shift each stack group by slot*gap (gap 2mm default, 5mm if crowded), FR4 = slot 0:+  components +5 | top silk +4 | top mask +3 | top copper +2 | inner L2 +1 quad+  [FR4 core 0]+  inner L15 -1 quad | bottom copper -2 | bottom mask -3 | bottom silk -4+Real bodies move by node translation (top-level groups: Board, 1-copper, 16-copper, 1-soldermask,+16-soldermask, Packages=all components); silk + inner layers are gerber quads. Keep the WHOLE+stackup present and in correct order. Do this on the raw GLB, then optimize (join keeps materials).++## Wiki page gotcha: the Overview README is the PACKAGE layer++`adom-wiki repo push README.md` updates the Files tab + assets the viewers fetch (GLB changes show+instantly) but does NOT refresh the page Overview README - that renders from the last `pkg publish`.+Bump the version in package.json (publish reads package.json, NOT page.json) and republish.+++## THE PHYSICAL FABRICATION STACK (golden, John 2026-07-15) - do not model FR4 as one slab++A multilayer board is NOT one FR4 slab with copper on it. It is built up in the fab as alternating+copper and dielectric, and the 3D view must show EACH dielectric sublayer at its REAL thickness:++```+4-layer:  Cu(L1) | prepreg | Cu(L2) | CORE | Cu(L3) | prepreg | Cu(L4)+6-layer:  Cu | pp | Cu | core | Cu | pp | Cu | core | Cu | pp | Cu     (extend the pattern)+8-layer:  ... keep extending: copper, dielectric, copper, dielectric ...+```++Fab reality: a thin CORE (rigid FR4) is etched with the two inner copper layers on its faces; then+PREPREG (bonding sheet) is epoxied on top and bottom, and the outer copper foil is pressed onto that.+So the dielectric between copper layers alternates prepreg (outer, thinner) / core (center, thicker).++The stack is in the EAGLE/Fusion design rules (this IS Fusion's stackup dialog, programmatically):+- `layerSetup` e.g. `(1+2*15+16)` - copper layer numbers, with `+` = prepreg bond, `*` = core bond.+  `(1+2*15+16)` = L1 [prepreg] L2 [CORE] L15 [prepreg] L16 (4-layer, symmetric).+- `mtCopper` - copper thickness per layer (e.g. 0.035 mm = 1 oz).+- `mtIsolate` - dielectric thicknesses (prepreg + core). NOTE: these can be left at 2-layer defaults+  (a leading 1.6mm is the default board thickness, not a real prepreg); if they don't reconcile with+  the measured board, fit a symmetric stack to the MEASURED FR4 total instead, keeping copper 0.035.++BQ25792 (measured FR4 dielectric = 1.516mm, fitted symmetric): top/bottom prepreg 0.200 each,+core 1.046, four copper 0.035. Real z (mm): L16 -0.035..0.000 | bot prepreg 0.000..0.200 |+L15 0.200..0.235 | core 0.235..1.281 | L2 1.281..1.316 | top prepreg 1.316..1.516 | L1 1.516..1.551.++## Building the exploded view with REAL thicknesses (the breakthrough view - almost no EDA tool does this)++Every element is its OWN slab at its EXACT thickness, separated by a uniform slot gap (5-6 mm). The+FR4 must be split into its sublayers (prepreg/core/prepreg) - clone the exported Board body 3x, each+z-scaled to that sublayer's thickness (scale_z = thickness / measured_FR4_thickness) and translated+to its slot center; make the sublabs slightly translucent so inner copper reads through. Order:+components / top silk / top mask / L1 copper / top prepreg / L2 copper / CORE / L15 copper /+bottom prepreg / L16 copper / bottom mask / bottom silk. Outer copper (L1/L16) are the real recolored+bodies; inner copper (L2/L15) are gerber-rendered quads (they aren't exported as bodies). Keep the+ORIGINAL Board body but recolor it alpha=0 (hidden) since the 3 slabs replace it. This makes Adom a+breakthrough: you can SEE the inner layers, which almost no EDA tool shows in 3D.++## Related+- `fusion-molecule-export` - the export/optimize pipeline these rules feed into.+- Issue adom/step2glb#158 - the service's `_board_bbox` still uses the whole-assembly XY+  ("nothing overhangs a PCB edge" is false) and a stack z that merged meshes pollute; fix is to key+  on the FR4/substrate body per rules 1-2.