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 25 file(s)
25 files changed
+140−39
BRIDGE_VERSION+1−1CHANGELOG.md+8addin/AdomBridge/AdomBridge.manifest+1−1addin/AdomBridge/commands/close_document.py+2−2addin/AdomBridge/commands/cloud_documents.py+4−4addin/AdomBridge/commands/export.py+1−1addin/AdomBridge/commands/import_file.py+2−2addin/AdomBridge/commands/open_electronics_file.py+6−6addin/AdomBridge/commands/open_lbr.py+1−1addin/AdomBridge/http_server.py+1−1bridge.json+1−1contracts/describe.json+1−1dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1describe.py+1−1install.sh+1−1package.json+1−1page.json+2−2rust/Cargo.lock+5−5rust/Cargo.toml+1−1rust/bridge/src/addin.rs+4rust/bridge/src/small_verbs.rs+46rust/bridge/src/stdshapes.rs+1−1rust/bridge/src/testproject.rs+38−4tests/open_refusal_drill.py+9BRIDGE_VERSION+1−1@@ -1 +1 @@-1.13.67+1.13.68
CHANGELOG.md+8@@ -3,6 +3,14 @@ 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.68 (2026-11-03)++`fusion_project_create` makes a design with its sheet and board linked (`linked: true`), through Fusion's own EAGLE import, so a part placed on the sheet goes on the board. It used to open two unlinked documents, which is now only the fallback when the import can't run (`linked: false`, `whyNotLinked`). If Adom Bridge is waiting for an OK, it answers `stillRunning` with a `runId`.++A script that opens a design and meets Fusion's "will be opened with warnings" notice no longer hangs until the call times out: the bridge answers the notice with OK and says so (`noticesAnswered`). From AI Flow.++Add-in 1.0.33: its text no longer names the retired `fusion_send_key` and `fusion_click_fusion`. Text only; Fusion needs one restart to load it.+ ## 1.13.67 (2026-11-03) The `fusion_open_board` and `fusion_open_schematic` refusal handling from 1.13.66 now runs on the path those verbs really take: in 1.13.66 the modal still hung the call.
addin/AdomBridge/AdomBridge.manifest+1−1@@ -6,7 +6,7 @@ "description": { "": "Adom Desktop Conduit bridge add-in. Exposes a localhost HTTP API for external automation of Fusion 360." },- "version": "1.0.32",+ "version": "1.0.33", "runOnStartup": true, "supportedOS": "windows" }
addin/AdomBridge/commands/close_document.py+2−2@@ -158,7 +158,7 @@ def handle_close_document(app: adsk.core.Application, args: dict) -> dict: return { "success": False, "error": f"Failed to close document: {e}",- "_hint": "Retry with {\"save\": false} to discard changes, or call fusion_screenshot_fusion to check for a blocking Save dialog and dismiss it with fusion_send_key.",+ "_hint": "Retry with {\"save\": false} to discard changes, or call fusion_screenshot_fusion to check for a blocking Save dialog and answer it with fusion_dismiss_blocking_dialogs.", } @@ -202,7 +202,7 @@ def handle_close_all_documents(app: adsk.core.Application, args: dict) -> dict: return { "success": False, "error": f"Failed to enumerate documents: {e}",- "_hint": "Retry with {\"save\": false} to discard changes, or call fusion_screenshot_fusion to check for a blocking Save dialog and dismiss it with fusion_send_key.",+ "_hint": "Retry with {\"save\": false} to discard changes, or call fusion_screenshot_fusion to check for a blocking Save dialog and answer it with fusion_dismiss_blocking_dialogs.", } for doc in snapshot:
addin/AdomBridge/commands/cloud_documents.py+4−4@@ -937,7 +937,7 @@ def handle_open_cloud_file(app: adsk.core.Application, args: dict) -> dict: return { "success": False, "error": f"Failed to open '{target.name}' after {len(attempts)} attempts: {last_err}",- "_hint": "Call fusion_screenshot to check for a recovery dialog or other modal, then fusion_send_key with {\"key\":\"escape\"} or click to dismiss it, then retry.",+ "_hint": "Call fusion_screenshot to check for a recovery dialog or other modal, then fusion_dismiss_blocking_dialogs to answer it, then retry.", "data": {"fileId": target.id, "attempts": attempts}, } @@ -1017,14 +1017,14 @@ def handle_open_cloud_file(app: adsk.core.Application, args: dict) -> dict: "steps": [ "Run desktop_list_windows to find the Fusion HWND", "Run desktop_screenshot_window with that HWND",- "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"} or fusion_click_fusion on the Cancel/X button",+ "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs", "Screenshot again to confirm the dialog is gone", ], "commonDialogs": [- {"name": "What to design?", "dismiss": "fusion_send_key {\"key\": \"escape\"} or click Cancel"},+ {"name": "What to design?", "dismiss": "fusion_dismiss_blocking_dialogs"}, {"name": "PCB out of date", "dismiss": "Click Update or X to dismiss"}, {"name": "Recovered Documents", "dismiss": "fusion_dismiss_recovery"},- {"name": "Save changes?", "dismiss": "fusion_send_key {\"key\": \"escape\"} to cancel"},+ {"name": "Save changes?", "dismiss": "fusion_dismiss_blocking_dialogs"}, ], }, }
addin/AdomBridge/commands/export.py+1−1@@ -23,7 +23,7 @@ _HINT_FORMAT_UNAVAILABLE = ( ) _HINT_EXPORT_FAILED = ( "If this persists, call fusion_screenshot_fusion to check for a blocking dialog, "- "then fusion_send_key with 'escape' or 'enter' to dismiss it. "+ "then fusion_dismiss_blocking_dialogs to answer it. " "Wrong-workspace errors: call fusion_show_3d_board first." ) _HINT_FILE_LOCKED = (
addin/AdomBridge/commands/import_file.py+2−2@@ -81,7 +81,7 @@ def handle_import_file(app: adsk.core.Application, args: dict) -> dict: return { "success": False, "error": f"Import returned null for {path.name}",- "_hint": "Call fusion_screenshot_fusion to check for a blocking dialog (e.g. unit prompt), then fusion_send_key with 'enter' to accept defaults, then retry.",+ "_hint": "Call fusion_screenshot_fusion to check for a blocking dialog (e.g. unit prompt), then fusion_dismiss_blocking_dialogs, then retry.", } # Activate the newly imported document so subsequent commands work on it@@ -103,5 +103,5 @@ def handle_import_file(app: adsk.core.Application, args: dict) -> dict: return { "success": False, "error": f"Import failed: {e}",- "_hint": "Call fusion_screenshot to check for a modal dialog, dismiss with fusion_send_key escape, then retry.",+ "_hint": "Call fusion_screenshot to check for a modal dialog, answer with fusion_dismiss_blocking_dialogs, then retry.", }
addin/AdomBridge/commands/open_electronics_file.py+6−6@@ -173,7 +173,7 @@ def _open_electronics(app: adsk.core.Application, file_path: str, file_type: str return { "success": False, "error": f"Failed to open {file_type}: {e}",- "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, then fusion_send_key escape to dismiss it, then retry.",+ "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, then fusion_dismiss_blocking_dialogs to answer it, then retry.", "data": { "filePath": file_path, "hint": ("PATH WITH SPACES? This verb now quotes the path and falls back to the "@@ -232,7 +232,7 @@ def _open_electronics(app: adsk.core.Application, file_path: str, file_type: str "steps": [ "Run desktop_list_windows to find the Fusion HWND", "Run desktop_screenshot_window with that HWND",- "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"} or fusion_click_fusion on the Cancel/X button",+ "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs", "Screenshot again to confirm the dialog is gone", ], },@@ -376,7 +376,7 @@ def handle_show_3d_board(app: adsk.core.Application, args: dict) -> dict: "data": { "activeWorkspace": new_ws, "nextSteps": [- "FIRST: Screenshot Fusion window to check for blocking dialogs โ dismiss with fusion_send_key escape",+ "FIRST: Screenshot Fusion window to check for blocking dialogs โ answer with fusion_dismiss_blocking_dialogs", "Use fusion_take_screenshot to capture the 3D board view", "Use fusion_show_2d_board to return to 2D layout", "Use fusion_close_document to close without saving when done",@@ -387,7 +387,7 @@ def handle_show_3d_board(app: adsk.core.Application, args: dict) -> dict: "steps": [ "Run desktop_list_windows to find the Fusion HWND", "Run desktop_screenshot_window with that HWND",- "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",+ "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs", "Screenshot again to confirm the dialog is gone", ], },@@ -521,7 +521,7 @@ def handle_import_electronics(app: adsk.core.Application, args: dict) -> dict: "steps": [ "Run desktop_list_windows to find the Fusion HWND", "Run desktop_screenshot_window with that HWND",- "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",+ "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs", "Screenshot again to confirm the dialog is gone", ], },@@ -652,7 +652,7 @@ def handle_show_schematic(app: adsk.core.Application, args: dict) -> dict: "required": True, "steps": [ "Run desktop_screenshot_window on the Fusion HWND",- "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",+ "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs", ], }, }
addin/AdomBridge/commands/open_lbr.py+1−1@@ -67,7 +67,7 @@ def handle_open_lbr(app: adsk.core.Application, args: dict) -> dict: return { "success": False, "error": f"Document.newDesignFromLocal failed: {e}",- "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, dismiss with fusion_send_key escape, then retry.",+ "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, answer with fusion_dismiss_blocking_dialogs, then retry.", "data": {"filePath": file_path}, }
addin/AdomBridge/http_server.py+1−1@@ -400,7 +400,7 @@ class AdomBridgeHandler(BaseHTTPRequestHandler): "Add-in is busy with a long-running command. Do NOT retry add-in commands. " "Do NOT press Escape โ the add-in is working, not stuck on a dialog. " "Commands that still work: fusion_state, fusion_screenshot_fusion, "- "fusion_click_fusion, fusion_send_key, fusion_close_window."+ "fusion_close_window." ), } if walk:
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.13.67",+ "version": "1.13.68", "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.",
contracts/describe.json+1−1@@ -20734,7 +20734,7 @@ "steps": "[str]" }, "statusVerb": null,- "summary": "A new, empty electronics design (the standard's project_create): an empty schematic and board written as EAGLE source in <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as the schematic's global attributes) and opened in Fusion as a new design, dialog-free. Save it to the cloud with fusion_save_to_cloud.",+ "summary": "A new, empty electronics design (the standard's project_create): an empty schematic and board written as EAGLE source in <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as the schematic's global attributes) and imported into Fusion as one design with its sheet and board linked, so a part placed on the sheet goes on the board (linked:true, about 30 s, through Fusion's own import in the background; Adom Bridge may ask for an OK, and then it answers stillRunning with a runId for fusion_eagle_import_status). If the import can't run, it opens the two files as unlinked documents and says why (linked:false, whyNotLinked). Save it to the cloud with fusion_save_to_cloud.", "timeoutSeconds": 180 }, {
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.13.67"+version = "1.13.68" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.67"+version = "1.13.68" edition = "2021" [dependencies]
describe.py+1−1@@ -394,7 +394,7 @@ _T = [ {"expectDocument": "the design's name (default the document in front)", "label": "added to the folder name", "outputDir": "default Documents/Adom Snapshots/<design>", "pdfs": "bool, default true"}, {"folder": "path", "files": "[path]", "pdfs": "{made: [path], skipped}", "notes": "[str]"}, 300, None, False, {"expectDocument": "BQ25792", "label": "before-reroute"}),- ('fusion_project_create', "A new, empty electronics design (the standard's project_create): an empty schematic and board written as EAGLE source in <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as the schematic's global attributes) and opened in Fusion as a new design, dialog-free. Save it to the cloud with fusion_save_to_cloud.",+ ('fusion_project_create', "A new, empty electronics design (the standard's project_create): an empty schematic and board written as EAGLE source in <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as the schematic's global attributes) and imported into Fusion as one design with its sheet and board linked, so a part placed on the sheet goes on the board (linked:true, about 30 s, through Fusion's own import in the background; Adom Bridge may ask for an OK, and then it answers stillRunning with a runId for fusion_eagle_import_status). If the import can't run, it opens the two files as unlinked documents and says why (linked:false, whyNotLinked). Save it to the cloud with fusion_save_to_cloud.", {"parentDir": "a folder on the Fusion machine (required)", "name": "the design's name (required)", "title": "str", "layers": "2|4|6|8, default 2", "textVariables": "{'REV': 'A'}", "fabRules": "a fab from adom/pcb-design-rules, recorded for later", "overwrite": "bool", "dryRun": "bool"}, {"project": "folder", "files": "[sch, brd]", "layers": "int", "layerSetup": "str", "steps": "[str]"}, 180, None, False, {"parentDir": "C:/Users/me/Documents/Boards", "name": "Sensor-Hub", "layers": 4, "title": "Sensor hub", "textVariables": {"REV": "A"}, "dryRun": True}),
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.67"+DASH_VERSION="1.13.68" 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.67",+ "version": "1.13.68", "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.67",+ "version": "1.13.68", "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" ]-}+}
rust/Cargo.lock+5−5@@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "adom-fusion-bridge"-version = "1.13.67"+version = "1.13.68" dependencies = [ "adom-eda-checks", "base64",@@ -299,7 +299,7 @@ dependencies = [ [[package]] name = "fusion-core"-version = "1.13.67"+version = "1.13.68" dependencies = [ "serde", "serde_json",@@ -310,7 +310,7 @@ dependencies = [ [[package]] name = "fusion-lbr"-version = "1.13.67"+version = "1.13.68" dependencies = [ "regex", "roxmltree",@@ -322,14 +322,14 @@ dependencies = [ [[package]] name = "fusion-os-mac"-version = "1.13.67"+version = "1.13.68" dependencies = [ "fusion-core", ] [[package]] name = "fusion-os-win"-version = "1.13.67"+version = "1.13.68" 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.67"+version = "1.13.68" edition = "2021" license = "MIT"
rust/bridge/src/addin.rs+4@@ -322,6 +322,10 @@ pub fn pass_through(cmd: &str, args: &Value, ctx: &Ctx) -> Value { if matches!(cmd, "open_board" | "open_schematic") { return crate::orchestrations::open_file_guarded(ctx, cmd, args, &|v: &str, a: Value| pass_through_checked(v, &a, ctx)); }+ if cmd == "run_modeling_script" {+ let watch = crate::small_verbs::watch_for_open_notice(&ctx.caller, &ctx.pids);+ return crate::small_verbs::finish_notice_watch(watch, pass_through_checked(cmd, args, ctx));+ } pass_through_checked(cmd, args, ctx) }
rust/bridge/src/small_verbs.rs+46@@ -490,6 +490,46 @@ pub fn finish_picker_watch(watch: Option<PickerWatch>, mut r: Value) -> Value { r } +/// While a script runs: Fusion's "board file has errors and will be opened with warnings" notice. A script+/// that opens such a design doesn't return until it's answered, and a call between machines gives up at 120 s+/// with nothing to say why (AI Flow, 2026-11-03). OK is its only answer, so the bridge gives it. A script+/// shorter than the first look costs nothing.+pub struct NoticeWatch { stop: std::sync::Arc<std::sync::atomic::AtomicBool>, answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>> }++pub fn watch_for_open_notice(caller: &fusion_core::ab::Caller, fusion_pids: &[u32]) -> Option<NoticeWatch> {+ let ab = fusion_core::ab::Loopback::from_env()?;+ let (stop, answered) = (std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), std::sync::Arc::new(std::sync::Mutex::new(Vec::new())));+ let (stop2, answered2, caller, pids) = (stop.clone(), answered.clone(), caller.clone(), fusion_pids.to_vec());+ std::thread::spawn(move || {+ use fusion_core::ab::AbClient;+ use std::sync::atomic::Ordering;+ let pause = |secs: u64| -> bool { for _ in 0..(secs * 2) { if stop2.load(Ordering::SeqCst) { return false; } std::thread::sleep(std::time::Duration::from_millis(500)); } true };+ if !pause(6) { return; }+ loop {+ for d in classify_blocking_dialogs(&ab, &caller, &pids) {+ if d["category"] != "board_opened_with_warnings" { continue; }+ if let Some(h) = d["hwnd"].as_i64() {+ let clicked = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK", "role": "Button"}), &caller, 15).map(|v| v["success"].as_bool() != Some(false)).unwrap_or(false);+ answered2.lock().unwrap().push(json!({"title": d["title"], "answeredWith": "OK", "clicked": clicked}));+ }+ }+ if !pause(3) { return; }+ }+ });+ Some(NoticeWatch { stop, answered })+}++pub fn finish_notice_watch(watch: Option<NoticeWatch>, mut r: Value) -> Value {+ let Some(w) = watch else { return r };+ w.stop.store(true, std::sync::atomic::Ordering::SeqCst);+ let answered = w.answered.lock().unwrap().clone();+ if !answered.is_empty() && r.is_object() {+ r["noticesAnswered"] = json!(answered);+ r["_hintNotice"] = json!("Fusion opened a board file with errors and said so; the bridge answered its notice with OK. Read the board before trusting it: fusion_routing_state lists any invalidViaNets, and fusion_set_layer_count at the same count repairs them.");+ }+ r+}+ /// Fusion's "<file> has errors and cannot be opened" modal, by its text: (hwnd, what it says). pub fn file_refused_dialog(ab: &dyn fusion_core::ab::AbClient, caller: &fusion_core::ab::Caller, fusion_pids: &[u32]) -> Option<(i64, String)> { let mut args = json!({});@@ -765,6 +805,12 @@ mod image_delivery_tests { assert_eq!(r["pickerClosed"], true); assert!(r["error"].as_str().unwrap().contains("C:/a/l.lbr and C:/b/l.lbr")); }+ #[test] fn an_answered_notice_is_said_in_the_reply() {+ let w = Some(NoticeWatch { stop: Default::default(), answered: std::sync::Arc::new(std::sync::Mutex::new(vec![json!({"answeredWith": "OK"})])) });+ let r = finish_notice_watch(w, json!({"success": true}));+ assert_eq!(r["noticesAnswered"][0]["answeredWith"], "OK");+ assert!(finish_notice_watch(None, json!({"success": true}))["noticesAnswered"].is_null());+ } #[test] fn no_picker_leaves_the_reply_alone() { assert_eq!(finish_picker_watch(watch(None), json!({"success": true}))["success"], true); assert_eq!(finish_picker_watch(None, json!({"success": true}))["success"], true);
rust/bridge/src/stdshapes.rs+1−1@@ -441,7 +441,7 @@ pub fn update_pcb(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value { json!({"success": true, "executed": execute && !behind, "changeOrder": [], "preview": {"addParts": add, "removeParts": remove, "sheetParts": cc["schematicComponents"], "boardParts": cc["boardComponents"]}, "sheet": cc["sheet"], "board": cc["board"],- "_hint": if behind { "addParts are on the sheet and not on the board. A supply symbol or frame has no package and stays that way; a part with a footprint means this design's board is not linked to its sheet (a fusion_project_create design): start from fusion_board_seed and fusion_new_electronics_from_eagle, which link them." } else { "Fusion keeps the board in step with the sheet as you edit, so there is nothing to run. removeParts are only on the board and stay there." }})+ "_hint": if behind { "addParts are on the sheet and not on the board. A supply symbol or frame has no package and stays that way; a part with a footprint means this design's board is not linked to its sheet (opened from loose EAGLE files, or a fusion_project_create that answered linked:false): fusion_project_create, or fusion_board_seed with fusion_new_electronics_from_eagle, make a linked one." } else { "Fusion keeps the board in step with the sheet as you edit, so there is nothing to run. removeParts are only on the board and stay there." }}) } /// `fusion_select_items` (the standard's select_items): select parts by designator, or clear the selection.
rust/bridge/src/testproject.rs+38−4@@ -317,8 +317,8 @@ pub(crate) fn board_layers() -> String { /// The standard's project_create: an empty schematic and board as EAGLE source in /// <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as-/// the schematic's global attributes), opened in Fusion as a new design through the dialog-free-/// Document.newDesignFromLocal. Saving it to the cloud is fusion_save_to_cloud.+/// the schematic's global attributes), imported into Fusion as a linked design, or opened as two unlinked+/// documents when the import can't run (linked:false). Saving it to the cloud is fusion_save_to_cloud. pub fn project_create(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value { let name = args["name"].as_str().unwrap_or("").trim().to_string(); let parent = args["parentDir"].as_str().unwrap_or("").replace('\\', "/");@@ -342,6 +342,40 @@ pub fn project_create(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Val let brd = format!("{HEAD}{brd_layers}<board><plain/><libraries/><attributes/><variantdefs/><classes><class number=\"0\" name=\"default\" width=\"0\" drill=\"0\"/></classes><designrules name=\"default\"><param name=\"layerSetup\" value=\"{setup}\"/></designrules><autorouter/><elements/><signals/></board></drawing></eagle>\n"); if let Err(e) = std::fs::create_dir_all(&dir) { return json!({"success": false, "errorCode": "write_failed", "error": format!("{dir}: {e}")}); } if let Err(e) = std::fs::write(&sch_path, &sch).and_then(|_| std::fs::write(&brd_path, &brd)) { return json!({"success": false, "errorCode": "write_failed", "error": e.to_string()}); }+ // Fusion's EAGLE import makes a design whose sheet and board are linked: a part placed on the sheet is on+ // the board (live 2026-11-03, 25 s on an empty project). Opening the two files with newDesignFromLocal+ // makes two unlinked documents, so that's only the fallback when the import can't run.+ let why_not_linked;+ if args["linked"].as_bool() != Some(false) {+ let started = crate::orchestrations::new_electronics_from_eagle(ctx, &json!({"schPath": sch_path, "brdPath": brd_path}), pass);+ if let Some(run) = started["runId"].as_str().filter(|_| started["success"].as_bool() == Some(true)) {+ let mut stage = Value::Null;+ let mut record = Value::Null;+ for _ in 0..30 {+ std::thread::sleep(Duration::from_secs(3));+ record = crate::orchestrations::eagle_import_status(ctx, &json!({"runId": run}));+ stage = record["stage"].clone();+ if stage == "done" || stage == "failed" { break; }+ }+ if stage == "done" {+ crate::chains::note_bridge_doc_pub(&ctx.state_dir, &name);+ let doc = pass("get_app_state", json!({}))["data"]["activeDocument"].clone();+ return json!({"success": true, "mutated": true, "linked": true, "documentName": doc, "project": dir, "files": [sch_path, brd_path], "layers": layers, "layerSetup": setup,+ "fabRules": args["fab"].clone().as_str().or(args["fabRules"].as_str()).map(|f| json!({"recorded": f, "apply": "fusion_set_design_rules {fab} once the board has content"})),+ "_hint": "An empty electronics design with its sheet and board linked: a part placed on the sheet goes on the board. Fusion names it by its own rules (documentName). Save it to the cloud with fusion_save_to_cloud {name, projectName, folderPath}; the EAGLE files stay in project as the source."});+ }+ if stage != "failed" {+ return json!({"success": true, "mutated": true, "stillRunning": true, "started": true, "runId": run, "statusVerb": "fusion_eagle_import_status", "project": dir, "files": [sch_path, brd_path], "layers": layers, "layerSetup": setup,+ "needsApproval": started["needsApproval"], "tellUser": started["tellUser"],+ "_hint": "Fusion's import is still running, likely waiting on Adom Bridge's approval to fill its Open dialog. Poll fusion_eagle_import_status {runId}."});+ }+ why_not_linked = json!(record["result"]["error"].as_str().unwrap_or("Fusion's EAGLE import failed"));+ } else {+ why_not_linked = json!(started["error"].as_str().unwrap_or("Fusion's EAGLE import could not start"));+ }+ } else {+ why_not_linked = json!("linked:false was asked for");+ } let mut steps: Vec<String> = Vec::new(); for (label, fpath) in [("board", &brd_path), ("schematic", &sch_path)] { let fp = crate::fusionpath::for_fusion(&ctx.state_dir, fpath).unwrap_or_else(|_| fpath.clone());@@ -355,7 +389,7 @@ pub fn project_create(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Val std::thread::sleep(Duration::from_secs(2)); } crate::chains::note_bridge_doc_pub(&ctx.state_dir, &name);- json!({"success": true, "mutated": true, "project": dir, "files": [sch_path, brd_path], "layers": layers, "layerSetup": setup, "steps": steps,+ json!({"success": true, "mutated": true, "linked": false, "whyNotLinked": why_not_linked, "project": dir, "files": [sch_path, brd_path], "layers": layers, "layerSetup": setup, "steps": steps, "fabRules": args["fab"].clone().as_str().or(args["fabRules"].as_str()).map(|f| json!({"recorded": f, "apply": "fusion_set_design_rules {fab} once the board has content"})),- "_hint": "An empty schematic and board, open in Fusion as a new design. Save it to the cloud with fusion_save_to_cloud {name, projectName, folderPath}; the EAGLE files stay in project as the source."})+ "_hint": "An empty schematic and board, open as two documents that are NOT linked (whyNotLinked): parts placed on the sheet stay off the board. Save it to the cloud with fusion_save_to_cloud {name, projectName, folderPath}; the EAGLE files stay in project as the source."}) }
tests/open_refusal_drill.py+9@@ -49,4 +49,13 @@ g = call('fusion_get_app_state'); check('Fusion still answers', g.get('success') r = call('fusion_sch_place_part', {'library': a.lib, 'symbol': 'RES', 'designator': 'R9', 'x': 3000, 'y': 4000}); check('another library still places', r.get('success'), r.get('error')) for n in docs(): if n != 'Untitled' or docs().count('Untitled') > 1: call('fusion_close_document', {'name': n})+print('--- fusion_project_create is linked')+for layers in (2, 4):+ r = call('fusion_project_create', {'name': 'OLink%d' % layers, 'parentDir': D, 'layers': layers, 'overwrite': True}); check('%d layers: linked' % layers, r.get('linked') is True and r.get('documentName'), {k: r.get(k) for k in ('success', 'linked', 'whyNotLinked', 'error', 'stillRunning')})+ time.sleep(2); no_dialogs('after it')+ call('fusion_show_schematic', {}); r = call('fusion_sch_place_part', {'library': a.lib, 'symbol': 'RES', 'designator': 'R1', 'x': 3000, 'y': 3000}); check('a part goes on the sheet', r.get('success'), r.get('error'))+ call('fusion_show_2d_board', {}); s = call('fusion_board_summary', {}); names = [c.get('designator') for c in (s.get('summary') or s).get('components') or []]+ check('and is on the board', 'R1' in names, {k: s.get(k) for k in ('success', 'errorCode', 'error')})+ d0 = call('fusion_get_app_state').get('data', {}).get('activeDocument'); n = len(call('fusion_routing_state', {'expectDocument': d0}).get('copperLayers') or []); check('with %d copper layers' % layers, n == layers, n)+ call('fusion_close_document', {'name': d0}) print('%d PASS, %d FAIL' % (sum(results), len(results) - sum(results))); sys.exit(0 if all(results) else 1)