app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
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.
← Commit history
Update 17 file(s)
17 files changed
+132−22
BRIDGE_VERSION+1−1CHANGELOG.md+11bridge.json+1−1dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1install.sh+1−1package.json+1−1page.json+1−1rust/Cargo.toml+1−1rust/bridge/src/checks.rs+2−1rust/bridge/src/main.rs+2−1rust/bridge/src/writers.rs+74rust/vendor/adom-eda-checks/Cargo.toml+5−2rust/vendor/adom-eda-checks/README.md+15−1rust/vendor/adom-eda-checks/VENDOR.md+3−3rust/vendor/adom-eda-checks/src/lib.rs+7−4rust/vendor/adom-eda-checks/src/main.rs+5−2BRIDGE_VERSION+1−1@@ -1 +1 @@-1.13.1+1.13.2
CHANGELOG.md+11@@ -3,6 +3,17 @@ 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.2 (2026-10-28, insiders)++The standard's shared writers, for files every bridge writes the same way.++- `fusion_export_gencad` is written by the EDA Bridge Standard's shared engine (adom-eda-checks 0.2.1,+ standard 0.5.1), in inches. Shapes are keyed by footprint and pad geometry, so two same-named footprints with+ different pads stay separate shapes.+- `fusion_export_dxf {layers}`: copper layers and Edge.Cuts come from the shared engine; silkscreen, fab and+ documentation layers stay on this bridge's writer, which also draws their texts.+- The engine's checks are unchanged.+ ## 1.13.1 (2026-10-28, insiders) More of the standard's schematic, document and board verbs. 298 verbs.
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.13.1",+ "version": "1.13.2", "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.1"+version = "1.13.2" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.1"+version = "1.13.2" 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.1"+DASH_VERSION="1.13.2" 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.1",+ "version": "1.13.2", "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+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.13.1",+ "version": "1.13.2", "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": {
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.1"+version = "1.13.2" edition = "2021" license = "MIT"
rust/bridge/src/checks.rs+2−1@@ -1,6 +1,7 @@ //! The EDA Bridge Standard's checks tier through the shared engine (adom-eda-checks, vendored in //! rust/vendor): the board model and its companions come from one export of the live board //! (routing's check_inputs), the engine computes, and the reply names this bridge's verbs.+pub const ENGINE: &str = "adom-eda-checks 0.2.1 (EDA Bridge Standard 0.5.1, vendored)"; use serde_json::{json, Value}; pub fn owns(cmd: &str) -> bool { adom_eda_checks::CHECKS.contains(&cmd) }@@ -42,6 +43,6 @@ pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Val if let Some(v) = r[key]["verb"].as_str().map(str::to_string) { r[key]["verb"] = json!(format!("fusion_{v}")); } } for k in ["boardId", "documentName", "revision"] { if !inputs[k].is_null() { r[k] = inputs[k].clone(); } }- r["engine"] = json!("adom-eda-checks 0.1.4 (EDA Bridge Standard 0.4.5, vendored)");+ r["engine"] = json!(ENGINE); r }
rust/bridge/src/main.rs+2−1@@ -13,6 +13,7 @@ //! - /command body {command, args}; the response is the verb's JSON as-is mod chains;+mod writers; mod pdf; mod small_verbs; mod addin;@@ -478,7 +479,7 @@ fn main() { let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe:h.fusion_executable(), short_path:|p|host().short_path(p), state_dir:h.bridge_state_dir(), caller:caller.clone(), pids:h.fusion_processes().iter().map(|p|p.pid).collect(), sidecar:&side, dialogs:&dlg }; let cmd=v.strip_prefix("fusion_").unwrap_or(v);- let mut r=match cmd { "bodies" => bodies::bodies(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "export_bom" => routing::export_bom(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "save_document" => savedoc::save_document(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "save_to_cloud" => savedoc::save_to_cloud(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if sch::owns(c) => sch::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if docs::owns(c) => docs::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if checks::owns(c) => checks::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), _ => routing::call(cmd,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)) };+ let mut r=match cmd { "bodies" => bodies::bodies(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "export_bom" => routing::export_bom(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "save_document" => savedoc::save_document(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), "save_to_cloud" => savedoc::save_to_cloud(&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if sch::owns(c) => sch::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if docs::owns(c) => docs::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if writers::owns(c) => writers::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), c if checks::owns(c) => checks::call(c,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)), _ => routing::call(cmd,&parsed["args"],&|v,a|addin::pass_through(v,&a,&ctx)) }; if cmd=="netlist" { r=sheet::shape_board_netlist(r,&parsed["args"]); } r["bridgeVersion"]=json!(VERSION); r["shell"]=json!("rust"); r },
rust/bridge/src/writers.rsadded+74@@ -0,0 +1,74 @@+//! fusion_export_gencad and fusion_export_dxf {layers} through the standard's shared writers+//! (adom-eda-checks, standard #16): the board model and Fusion's poured copper in, the file+//! text out, written here. DXF layers the engine does not draw with texts (silkscreen, fab,+//! documentation) stay on this bridge's own writer (routing_script board_dxf), so nothing is lost.+use serde_json::{json, Value};++pub fn owns(cmd: &str) -> bool { matches!(cmd, "export_gencad" | "board_dxf") }++fn safe(name: &str) -> String { name.chars().map(|c| if c.is_ascii_alphanumeric() || "-_.".contains(c) { c } else { '_' }).collect() }++fn out_dir(args: &Value, default: &str) -> String {+ args["outputDir"].as_str().map(|s| s.replace('\\', "/")).unwrap_or_else(|| std::env::temp_dir().join(default).to_string_lossy().replace('\\', "/"))+}++/// Copper and the outline go to the engine; everything else stays on the bridge writer.+pub fn engine_layer(l: &str) -> bool { l == "Edge.Cuts" || l.ends_with(".Cu") }++pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+ let board = crate::routing::call("routing_state", args, pass);+ if board["success"].as_bool() == Some(false) { return board; }+ let doc = board["documentName"].as_str().unwrap_or("board").to_string();+ match cmd {+ "export_gencad" => {+ let r = adom_eda_checks::run("export_gencad", &json!({"board": board, "args": args}));+ let Some(text) = r["text"].as_str() else { return r };+ let dir = out_dir(args, "adom-gencad");+ let _ = std::fs::create_dir_all(&dir);+ let path = format!("{dir}/{}.cad", safe(&doc));+ if let Err(e) = std::fs::write(&path, text) { return json!({"success": false, "errorCode": "write_failed", "error": format!("{path}: {e}")}); }+ json!({"success": true, "mutated": false, "documentName": doc, "path": path, "format": "GenCAD 1.4", "units": r["units"], "counts": r["counts"],+ "components": r["counts"]["components"], "signals": r["counts"]["signals"], "shapes": r["counts"]["shapes"], "pads": r["counts"]["pads"],+ "writtenFrom": r["writtenFrom"], "engine": crate::checks::ENGINE,+ "_hint": "Written by the EDA Bridge Standard's shared writer (adom-eda-checks) from Fusion's board model, in inches; a bottom part is MIRRORX FLIP, as KiCad writes it."})+ }+ _ => {+ let want: Vec<String> = args["layers"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default();+ let (eng, own): (Vec<String>, Vec<String>) = want.iter().cloned().partition(|l| engine_layer(l));+ let dir = out_dir(args, "adom-board-dxf");+ let _ = std::fs::create_dir_all(&dir);+ let mut files: Vec<Value> = Vec::new();+ let mut notes: Vec<Value> = Vec::new();+ let mut as_drawn = Value::Null;+ if !eng.is_empty() {+ let pours = crate::routing::call("pour_state", args, pass);+ let mut a = args.clone(); a["layers"] = json!(eng);+ let r = adom_eda_checks::run("export_dxf", &json!({"board": board, "pours": pours, "args": a}));+ for f in r["files"].as_array().cloned().unwrap_or_default() {+ let path = format!("{dir}/{}-{}", safe(&doc), f["name"].as_str().unwrap_or("layer.dxf"));+ if let Err(e) = std::fs::write(&path, f["text"].as_str().unwrap_or("")) { return json!({"success": false, "errorCode": "write_failed", "error": format!("{path}: {e}")}); }+ files.push(json!({"layer": f["layer"], "path": path, "entities": f["entities"], "writer": "adom-eda-checks"}));+ }+ if let Some(u) = r["unknownLayers"].as_array() { if !u.is_empty() { notes.push(json!({"unknownLayers": u})); } }+ as_drawn = r["poursAsDrawn"].clone();+ }+ if !own.is_empty() {+ let mut a = args.clone(); a["layers"] = json!(own); a["outputDir"] = json!(dir);+ let r = crate::routing::call("board_dxf", &a, pass);+ if r["success"].as_bool() == Some(false) { return r; }+ for f in r["files"].as_array().cloned().unwrap_or_default() { let mut f = f; f["writer"] = json!("fusion-bridge"); files.push(f); }+ }+ json!({"success": !files.is_empty(), "mutated": false, "documentName": doc, "files": files, "poursAsDrawn": as_drawn, "notes": notes, "units": "mm",+ "engine": crate::checks::ENGINE,+ "_hint": "Copper and Edge.Cuts are written by the standard's shared writer from the board model with Fusion's poured copper; silkscreen, fab and documentation layers by this bridge's writer, which also draws their texts."})+ }+ }+}++#[cfg(test)] mod tests {+ use super::*;+ #[test] fn copper_and_outline_go_to_the_engine() {+ assert!(engine_layer("F.Cu") && engine_layer("In2.Cu") && engine_layer("Edge.Cuts"));+ assert!(!engine_layer("F.SilkS") && !engine_layer("F.Fab") && !engine_layer("Dwgs.User"));+ }+}
rust/vendor/adom-eda-checks/Cargo.toml+5−2@@ -1,8 +1,8 @@ [package] name = "adom-eda-checks"-version = "0.1.4"+version = "0.2.1" edition = "2021"-description = "The EDA Bridge Standard's checks, computed from the board model JSON: no bridge calls, no tool."+description = "The EDA Bridge Standard's checks and board-file writers, computed from the board model JSON: no bridge calls, no tool." license = "MIT" [lib]@@ -14,6 +14,9 @@ path = "src/main.rs" [dependencies] serde_json = "1"+# the SVG parser behind writers::svgart (pure Rust)+roxmltree = "0.20"+svgtypes = "0.15" [profile.release] opt-level = 3
rust/vendor/adom-eda-checks/README.md+15−1@@ -1,4 +1,4 @@-# adom-eda-checks: the shared checks engine+# adom-eda-checks: the shared checks engine and board-file writers The checks tier of the EDA Bridge Standard, computed from the board model and its companions as plain JSON. It makes no bridge calls, runs no tool and does no I/O. A bridge links the crate (the Altium, KiCad and Fusion bridges are all@@ -20,6 +20,18 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. The other checks-tier verbs (assembly, 3D heights, placement, schematic review, pair routing, length tuning, designators, via fences, panels, the board report) come over from the Altium bridge a module at a time. +## Writers++For a bridge whose tool has no native exporter (`writers`, from altium-lane, #16). They stay native-tier verbs in the+standard: a bridge whose tool can write the file keeps its own exporter. No I/O: each returns the file text or the+regions, with `writtenFrom`, and the bridge writes the file or places the regions.++| Verb | Reads | What it answers |+|---|---|---|+| `export_gencad` | board | GenCAD 1.4 text, in inches: outline, pads and padstacks, shapes, components (a bottom part `MIRRORX FLIP`, as KiCad writes it), devices, signals, routes |+| `export_dxf` | board, pours? | one DXF (R12) per layer in `args.layers`: Edge.Cuts as lines and arcs or one closed polyline (`contours`), copper as tracks, arcs, pads, vias and pours. With `pours` a copper layer shows the copper actually poured; without, `poursAsDrawn` names the layers that show the pour as drawn |+| `import_svg` | svg (text) | `regions` in board mm (y up) for a layer: each shape with its holes as one contour reaching the holes through zero-width slits |+ ## Input Every check takes one JSON object:@@ -61,6 +73,8 @@ As a command: adom-eda-checks list adom-eda-checks return_path --board board.json --stack stack.json --pours pours.json --args '{"resolutionMm": 0.1}' adom-eda-checks fab_check bundle.json # or - for stdin+adom-eda-checks export_dxf --board board.json --pours pours.json --args '{"layers": ["Edge.Cuts", "F.Cu"]}'+adom-eda-checks import_svg --svg logo.svg --args '{"at": [10, 10], "widthMm": 20, "layer": "F.SilkS"}' ``` Replies keep the standard envelope (`success`, `errorCode`, `error`, `_hint`) and name verbs without a prefix
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.4.5-- Standard commit: bb2b810d9ac934c5e65438a7cd4441bb81d41256-- Crate version: 0.1.4+- Standard version: 0.5.1+- Standard commit: e8a33ccecec060707f651b3956aeaa5492645290+- Crate version: 0.2.1 - Copied: 2026-10-28, 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/lib.rs+7−4@@ -1,6 +1,7 @@ //! adom-eda-checks: the EDA Bridge Standard's checks tier, computed from the board model and its companions as plain-//! JSON. No bridge calls, no tool, no I/O: a bridge links this crate (or runs the `adom-eda-checks` command) and-//! answers the check under its own prefix (`kicad_return_path`). Replies keep the standard envelope (`success`,+//! JSON, plus board-file writers (`writers`: GenCAD, DXF, SVG to regions) for a bridge whose tool has no native+//! exporter. No bridge calls, no tool, no I/O: a bridge links this crate (or runs the `adom-eda-checks` command) and+//! answers the verb under its own prefix (`kicad_return_path`). Replies keep the standard envelope (`success`, //! `errorCode`, `error`, `_hint`) and name verbs without a prefix. //! //! Every check takes one input bundle, `{board, stack?, pours?, rules?, silk?, fab?, bodies?, sheetParts?, netlist?, args?}` (see `input`).@@ -12,18 +13,20 @@ pub mod fab; pub mod geom; pub mod impedance; pub mod input;+pub mod writers; /// The checks this build answers, by standard verb name. pub const CHECKS: &[&str] = &["copper_balance", "current_capacity", "fab_check", "impedance", "return_path"]; -/// Run one check by its standard name.+/// Run one check or writer by its standard name. pub fn run(check: &str, input: &Value) -> Value {+ if let Some(out) = writers::run(check, input) { return out; } match check { "copper_balance" => coppermap::copper_balance(input), "current_capacity" => current::current_capacity(input), "fab_check" => fab::fab_check(input), "impedance" => impedance::impedance(input), "return_path" => coppermap::return_path(input),- _ => input::refuse("not_supported", &format!("{check} is not a check this engine answers (it answers {})", CHECKS.join(", "))),+ _ => input::refuse("not_supported", &format!("{check} is not a verb this engine answers (checks: {}; writers: {})", CHECKS.join(", "), writers::WRITERS.join(", "))), } }
rust/vendor/adom-eda-checks/src/main.rs+5−2@@ -1,5 +1,6 @@ //! adom-eda-checks <check> [bundle.json | -] //! adom-eda-checks <check> --board b.json [--stack s.json] [--pours p.json] [--args '{...}']+//! adom-eda-checks import_svg --svg art.svg --args '{"at": [10, 10], "widthMm": 20}' //! Prints the check's reply as JSON. `adom-eda-checks list` names the checks. use serde_json::{json, Value}; use std::io::Read;@@ -27,6 +28,8 @@ fn bundle(rest: &[String]) -> Result<Value, String> { b[key] = read(val)?; } "--args" => b["args"] = serde_json::from_str(val).map_err(|e| format!("--args: {e}"))?,+ // SVG artwork is text, not JSON+ "--svg" => b["svg"] = Value::String(std::fs::read_to_string(val).map_err(|e| format!("{val}: {e}"))?), _ => return Err(format!("unknown flag {flag}")), } }@@ -36,8 +39,8 @@ fn bundle(rest: &[String]) -> Result<Value, String> { fn main() { let argv: Vec<String> = std::env::args().skip(1).collect(); let out = match argv.first().map(String::as_str) {- None | Some("-h") | Some("--help") => json!({"success": true, "usage": "adom-eda-checks <check> [bundle.json | -] | <check> --board b.json [--stack s.json] [--pours p.json] [--args '{...}']", "checks": adom_eda_checks::CHECKS}),- Some("list") => json!({"success": true, "checks": adom_eda_checks::CHECKS}),+ None | Some("-h") | Some("--help") => json!({"success": true, "usage": "adom-eda-checks <check> [bundle.json | -] | <check> --board b.json [--stack s.json] [--pours p.json] [--args '{...}']", "checks": adom_eda_checks::CHECKS, "writers": adom_eda_checks::writers::WRITERS}),+ Some("list") => json!({"success": true, "checks": adom_eda_checks::CHECKS, "writers": adom_eda_checks::writers::WRITERS}), Some(check) => match bundle(&argv[1..]) { Ok(b) => adom_eda_checks::run(check, &b), Err(e) => json!({"success": false, "errorCode": "bad_args", "error": e, "_hint": "Pass one bundle file {board, stack, pours, args}, or --board, --stack, --pours and --args."}),