← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.286\ No newline at end of file+1.9.287\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.286",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.286/adom-bridge-fusion-v1.9.286.zip",-  "sha256": "7665f021c00f9ba0d7c12a13f4def1d6836b252fa4ef5bed5bbd2071857443d5",-  "size": 578170,+  "version": "1.9.287",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.287/adom-bridge-fusion-v1.9.287.zip",+  "sha256": "5d1a0c8c8a168f5c766fe30d4a20b1cbd3b95fac28f027d5cae7af5bad8a4b06",+  "size": 578906,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.286",+  "version": "1.9.287",   "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/fusion-bridge",   "author": "Adom Inc.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.9.286"+version = "1.9.287" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.286"+version = "1.9.287" edition = "2021"  [dependencies]
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.286"+DASH_VERSION="1.9.287" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
library_bundle.py+85−52
@@ -78,35 +78,53 @@ def pair_parts(lbr_path: str, models: list, metadata: dict) -> dict:     the binding pass covers every device on it). Returns {parts:[{package, lbrPath, modelPath,     matchedBy}], unbound:[{package, why}], shared:[{deviceset, package, via}], models}.     """-    devicesets = [d.get("name") or "" for d in (metadata.get("devicesets") or [])]-    packages = {}+    # ONE PART PER (deviceset, PACKAGE), not per deviceset (John, 2026-08-30): a deviceset+    # may carry package VARIANTS (LAN7800 "", "-HV", "-SB" on QFN-48_7x7mm, -HV, -SB), each+    # bound to its own STEP so Change Package swaps footprint and 3D together. The 3D package+    # is per PACKAGE, so every distinct package is its own mint; devicesets sharing a package+    # still share the mint (shared[]).+    units = []          # (deviceset, deviceName, package)+    devicesets = []     for d in metadata.get("devicesets") or []:-        pk = ""+        ds = d.get("name") or ""+        devicesets.append(ds)+        seen = set()         for v in d.get("variants") or []:-            if v.get("package"):-                pk = v["package"]-                break-        packages[d.get("name") or ""] = pk+            pk = v.get("package") or ""+            if not pk or pk in seen:+                continue+            seen.add(pk)+            units.append((ds, str(v.get("device") or ""), pk))+        if not seen:+            units.append((ds, "", ""))+    packages = {ds: next((pk for (d2, _n, pk) in units if d2 == ds and pk), "") for ds in devicesets}     by_stem = {_stem(m): m for m in models}     by_norm = {_norm(_stem(m)): m for m in models}     parts, unbound, used = [], [], set() -    def _take(m, how, ds):+    def _take(m, how, ds, pk="", dev=""):         used.add(m)-        parts.append({"deviceset": ds, "package": packages.get(ds) or ds, "lbrPath": lbr_path,-                      "modelPath": m, "matchedBy": how})+        parts.append({"deviceset": ds, "device": dev, "package": pk or packages.get(ds) or ds,+                      "lbrPath": lbr_path, "modelPath": m, "matchedBy": how})      shared = []     bound_pkgs = {}-    for ds in devicesets:-        pk = packages.get(ds) or ""+    for (ds, dev, pk) in units:         if pk and pk in bound_pkgs:             # Several devicesets on ONE package (symbol variants, value clones): the 3D package             # is per PACKAGE, so the mint done for the first deviceset serves all of them and the             # binding pass writes the urn into every device that uses the package.             shared.append({"deviceset": ds, "package": pk, "via": bound_pkgs[pk]})             continue-        cands = [(ds, "deviceset"), (pk, "package")] if pk else [(ds, "deviceset")]+        # Match order: package name (the variant's own identity), deviceset+device name+        # (LAN7800-HV), then the bare deviceset (baseline device only).+        cands = []+        if pk:+            cands.append((pk, "package"))+        if dev:+            cands.append((ds + dev, "deviceset+device"))+        else:+            cands.append((ds, "deviceset"))         hit = None         for name, how in cands:             if name in by_stem and by_stem[name] not in used:@@ -125,15 +143,16 @@ def pair_parts(lbr_path: str, models: list, metadata: dict) -> dict:                 if hit:                     break         if hit:-            _take(hit[0], hit[1], ds)+            _take(hit[0], hit[1], ds, pk, dev)             if pk:                 bound_pkgs[pk] = ds         else:-            unbound.append({"package": ds, "why": "no STEP in the bundle named like the deviceset"-                                                   + (" or its package %r" % pk if pk else "")})-    if not parts and len(devicesets) == 1 and len(models) == 1:+            unbound.append({"package": pk or ds, "deviceset": ds, "device": dev,+                            "why": "no STEP in the bundle named like the package %r" % pk+                                   + (" or the device %r" % (ds + dev))})+    if not parts and len(units) == 1 and len(models) == 1:         unbound.clear()-        _take(models[0], "only-one-of-each", devicesets[0])+        _take(models[0], "only-one-of-each", units[0][0], units[0][2], units[0][1])     return {"parts": parts, "unbound": unbound, "shared": shared, "models": models,             "modelsUnused": [m for m in models if m not in used]} @@ -190,38 +209,52 @@ def split_per_part(lbr_path: str, out_dir: str) -> dict:             sy = g.get("symbol")             if sy and sy not in syms:                 syms.append(sy)-        new_root = copy.deepcopy(root)-        new_lib = new_root.find(".//library")-        for tag in ("packages", "symbols", "devicesets", "packages3d"):-            el = new_lib.find(tag)-            if el is not None:-                new_lib.remove(el)-        pe = ET.SubElement(new_lib, "packages")-        for pk in pkgs:-            if pk in pk_all:-                pe.append(copy.deepcopy(pk_all[pk]))-        se = ET.SubElement(new_lib, "symbols")-        for sy in syms:-            if sy in sy_all:-                se.append(copy.deepcopy(sy_all[sy]))-        de = ET.SubElement(new_lib, "devicesets")-        ds2 = copy.deepcopy(ds)-        for inst in ds2.iter("package3dinstances"):-            pass-        for dev in ds2.findall("./devices/device"):-            for inst in dev.findall("package3dinstances"):-                dev.remove(inst)-        de.append(ds2)-        # EAGLE order inside <library>: description?, packages, symbols, devicesets-        children = list(new_lib)-        head = [c for c in children if c.tag not in ("packages", "symbols", "devicesets")]-        for c in children:-            new_lib.remove(c)-        for c in head + [pe, se, de]:-            new_lib.append(c)-        safe = re.sub(r"[^A-Za-z0-9._-]+", "_", name) or "part"-        fp = os.path.join(out_dir, safe + ".lbr").replace("\\", "/")-        ET.ElementTree(new_root).write(fp, encoding="utf-8", xml_declaration=True)-        out[name] = {"lbrPath": fp, "package": pkgs[0] if pkgs else "", "symbols": syms,-                     "packages": pkgs}+        # ONE FILE PER (deviceset, PACKAGE) (John, 2026-08-30): a deviceset with package+        # VARIANTS (LAN7800 "", "-HV", "-SB") needs each variant minted on a library whose+        # FIRST (and only) package is THAT variant's footprint, with only the devices that+        # use it. out[deviceset] keeps pointing at the primary package's file for callers+        # that think per deviceset; out[deviceset + "::" + package] is the per-variant key.+        for which, only_pk in [(None, None)] + [(pk, pk) for pk in pkgs]:+            new_root = copy.deepcopy(root)+            new_lib = new_root.find(".//library")+            for tag in ("packages", "symbols", "devicesets", "packages3d"):+                el = new_lib.find(tag)+                if el is not None:+                    new_lib.remove(el)+            use_pkgs = [only_pk] if only_pk else pkgs+            pe = ET.SubElement(new_lib, "packages")+            for pk in use_pkgs:+                if pk in pk_all:+                    pe.append(copy.deepcopy(pk_all[pk]))+            se = ET.SubElement(new_lib, "symbols")+            for sy in syms:+                if sy in sy_all:+                    se.append(copy.deepcopy(sy_all[sy]))+            de = ET.SubElement(new_lib, "devicesets")+            ds2 = copy.deepcopy(ds)+            devs = ds2.find("./devices")+            for dev in list(ds2.findall("./devices/device")):+                for inst in dev.findall("package3dinstances"):+                    dev.remove(inst)+                if only_pk and dev.get("package") != only_pk and devs is not None:+                    devs.remove(dev)+            de.append(ds2)+            # EAGLE order inside <library>: description?, packages, symbols, devicesets+            children = list(new_lib)+            head = [c for c in children if c.tag not in ("packages", "symbols", "devicesets")]+            for c in children:+                new_lib.remove(c)+            for c in head + [pe, se, de]:+                new_lib.append(c)+            safe = re.sub(r"[^A-Za-z0-9._-]+", "_", name) or "part"+            if only_pk:+                safe += "__" + (re.sub(r"[^A-Za-z0-9._-]+", "_", only_pk) or "pkg")+            fp = os.path.join(out_dir, safe + ".lbr").replace("\\", "/")+            ET.ElementTree(new_root).write(fp, encoding="utf-8", xml_declaration=True)+            entry = {"lbrPath": fp, "package": (only_pk or (pkgs[0] if pkgs else "")),+                     "symbols": syms, "packages": use_pkgs}+            if only_pk:+                out[name + "::" + only_pk] = entry+            else:+                out[name] = entry     return out
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.286",+  "version": "1.9.287",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+11−8
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.286",+  "version": "1.9.287",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -75,7 +79,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -104,9 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
server.py+3−1
@@ -13651,7 +13651,9 @@ def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict:         parts = []         for p in pairing["parts"]:             _ds = p.get("deviceset") or p["package"]-            _sp = _split.get(_ds) or {}+            # per-variant key first (deviceset::package), then the deviceset's primary file+            _sp = (_split.get(_ds + "::" + (p.get("package") or "")) if p.get("package") else None) \+                  or _split.get(_ds) or {}             if not _sp.get("lbrPath") or not _sp.get("package"):                 pairing["unbound"].append({"package": _ds, "why": "could not split this deviceset "                                            "into its own library (no package on its devices?)"})