← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.25\ No newline at end of file+1.13.26\ No newline at end of file
CHANGELOG.md+6
@@ -3,6 +3,12 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.13.26 (2026-10-30)++- Engine 0.21.1 (standard 0.24.2): lint_library's duplicate_pad_number counts same-numbered pads that touch or overlap+  as one land, so a split exposed pad is no longer a false warning. Otherwise as 1.13.25, which this replaces before+  it was smoke-tested.+ ## 1.13.25 (2026-10-30)  - Engine 0.21.0 (standard 0.24.0). `fusion_lint_library` (the standard's lint_library): a .lbr read offline (pads,
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.25",+  "version": "1.13.26",   "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.13.25"+version = "1.13.26" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.25"+version = "1.13.26" 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.13.25"+DASH_VERSION="1.13.26" 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
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.25",+  "version": "1.13.26",   "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+2−2
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.25",+  "version": "1.13.26",   "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": {@@ -152,4 +152,4 @@   "confirmed_user_skills": [     "fusion-design-efficiency"   ]-}\ No newline at end of file+}
rust/Cargo.lock+6−6
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-eda-checks"-version = "0.21.0"+version = "0.21.1" dependencies = [  "roxmltree",  "serde_json",@@ -19,7 +19,7 @@ dependencies = [  [[package]] name = "adom-fusion-bridge"-version = "1.13.25"+version = "1.13.26" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.25"+version = "1.13.26" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.25"+version = "1.13.26" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.25"+version = "1.13.26" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.25"+version = "1.13.26" dependencies = [  "fusion-core",  "windows-sys 0.59.0",
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.13.25"+version = "1.13.26" edition = "2021" license = "MIT" 
rust/vendor/adom-eda-checks/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.21.0"+version = "0.21.1" edition = "2021" description = "The EDA Bridge Standard's checks and board-file writers, computed from the board model JSON: no bridge calls, no tool." license = "MIT"
rust/vendor/adom-eda-checks/VENDOR.md+3−3
@@ -3,9 +3,9 @@ The EDA Bridge Standard's shared checks engine, copied unchanged from adom/eda-bridge-standard `engine/` (the wiki has no git protocol, so Cargo cannot fetch it). -- Standard version: 0.24.0-- Standard commit: aaeef001d59d48d93e08f58edc0cc705cd5e8c80-- Crate version: 0.21.0+- Standard version: 0.24.2+- Standard commit: d104b57e76bd000fd5271711ee15c85e01deeaf6+- Crate version: 0.21.1 - Copied: 2026-10-30, by fusion-lane  To update: clone the standard fresh (`adom-wiki repo clone adom/eda-bridge-standard`), copy `engine/src`,
rust/vendor/adom-eda-checks/src/liblint.rs+35−3
@@ -57,6 +57,22 @@ fn extent(c: &Value) -> Option<(f64, f64, f64, f64)> {     None } +/// A pad's copper box (the hole's, if larger), in the footprint's frame.+fn pad_box(p: &Value) -> (f64, f64, f64, f64) {+    let (x, y) = (f(&p["x"]), f(&p["y"]));+    let (w, h) = (f(&p["w"]).max(f(&p["hole"])) / 2.0, f(&p["h"]).max(f(&p["hole"])) / 2.0);+    (x - w, y - h, x + w, y + h)+}++/// How many separate lands the boxes make: boxes that touch or overlap (within 0.01 mm) join.+fn lands(boxes: &[(f64, f64, f64, f64)]) -> usize {+    let mut group: Vec<usize> = (0..boxes.len()).collect();+    fn root(g: &mut Vec<usize>, i: usize) -> usize { let mut i = i; while g[i] != i { g[i] = g[g[i]]; i = g[i]; } i }+    let touch = |a: &(f64, f64, f64, f64), b: &(f64, f64, f64, f64)| a.0 <= b.2 + 0.01 && b.0 <= a.2 + 0.01 && a.1 <= b.3 + 0.01 && b.1 <= a.3 + 0.01;+    for i in 0..boxes.len() { for j in i + 1..boxes.len() { if touch(&boxes[i], &boxes[j]) { let (a, b) = (root(&mut group, i), root(&mut group, j)); group[a] = b; } } }+    (0..boxes.len()).filter(|&i| root(&mut group, i) == i).count()+}+ fn finding(name: &str, kind: &str, rule: &str, detail: String) -> Value {     json!({"name": name, "kind": kind, "rule": rule, "severity": severity(rule), "detail": detail}) }@@ -67,15 +83,20 @@ pub fn lint_footprint(fp: &Value) -> Vec<Value> {     let mut out = Vec::new();     let pads = fp["pads"].as_array().cloned().unwrap_or_default();     if pads.is_empty() { out.push(finding(&name, "footprint", "no_pads", "the footprint has no pads".into())); }-    let mut seen: BTreeMap<String, usize> = BTreeMap::new();+    let mut seen: BTreeMap<String, Vec<(f64, f64, f64, f64)>> = BTreeMap::new();     for p in &pads {         let (w, h, hole) = (f(&p["w"]), f(&p["h"]), f(&p["hole"]));         if (w <= 0.0 || h <= 0.0) && hole <= 0.0 { out.push(finding(&name, "footprint", "zero_size_pad", format!("pad {} is {w} x {h} mm", text(&p["number"])))); }         let n = text(&p["number"]);         if n.is_empty() { if !mechanical(p) { out.push(finding(&name, "footprint", "unnumbered_pad", format!("a pad at ({}, {}) has no number", f(&p["x"]), f(&p["y"])))); } }-        else { *seen.entry(n).or_default() += 1; }+        else if !mechanical(p) { seen.entry(n).or_default().push(pad_box(p)); }+    }+    // pads sharing a number that touch or overlap are one land (a split exposed pad, thermal vias in it): a duplicate+    // is a number on lands apart from each other+    for (n, boxes) in &seen {+        let lands = lands(boxes);+        if lands > 1 { out.push(finding(&name, "footprint", "duplicate_pad_number", format!("pad number {n} is on {lands} separate lands ({} pads)", boxes.len()))); }     }-    for (n, k) in &seen { if *k > 1 { out.push(finding(&name, "footprint", "duplicate_pad_number", format!("{k} pads are numbered {n} (fine for a split thermal pad, a mistake otherwise)"))); } }     let court = fp.get("courtyard");     if court.map(empty).unwrap_or(false) { out.push(finding(&name, "footprint", "no_courtyard", "no courtyard".into())); }     // the origin should sit on the part: inside the pads and courtyard, grown by the margin@@ -193,6 +214,17 @@ mod tests {         assert!(off["detail"].as_str().unwrap().starts_with("the origin is 1268.700 mm"), "{off}");     } +    #[test]+    fn a_split_exposed_pad_is_one_land() {+        // KiCad's Package_SO: an exposed pad 17 as four tiles and two thermal vias, plus two pins both numbered 1+        let ep: Vec<Value> = [(-0.6, -0.6), (0.6, -0.6), (-0.6, 0.6), (0.6, 0.6)].iter().map(|&(x, y)| json!({"number": "17", "x": x, "y": y, "w": 1.2, "h": 1.2})).chain([json!({"number": "17", "x": 0.0, "y": 0.0, "w": 0.6, "h": 0.6, "hole": 0.3}), json!({"number": "17", "x": 0.3, "y": 0.0, "w": 0.0, "h": 0.0, "mechanical": true})]).collect();+        let mut pads = ep.clone();+        pads.extend([pad("1", -2.7, 1.9), pad("1", 2.7, 1.9)]);+        let r = lint_library(&json!({"library": {"footprints": [{"name": "HTSSOP", "pads": pads}]}}));+        let d: Vec<&str> = r["findings"].as_array().unwrap().iter().filter(|x| x["rule"] == "duplicate_pad_number").map(|x| x["detail"].as_str().unwrap()).collect();+        assert_eq!(d, vec!["pad number 1 is on 2 separate lands (2 pads)"], "{r}");+    }+     #[test]     fn a_spare_pad_warns_and_a_mechanical_one_does_not() {         let fp = json!({"name": "USB", "pads": [pad("1", 0.0, 0.0), pad("2", 0.65, 0.0), pad("S1", 3.0, 0.0), json!({"number": "", "x": -2.0, "y": 1.0, "w": 0.0, "h": 0.0, "hole": 0.65, "plated": false})]});