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.
Report a library Fusion refuses instead of calling it opened #35
Fusion can refuse a library while the bridge reports that it opened. This makes that refusal visible.
What happened
A 7-part Cloud Library went through fusion_build_library_3d. The run reported stage: done, 7/7 bound and opened: true, while Fusion showed "<file>.lbr has errors and cannot be opened" over an empty design. fusion_preflight_lbr had passed the same file.
The file had 3D bindings naming packages it did not contain. The wiki sent each part's name as package, and inject_package3d_bindings wrote it into <packageinstance> as given. That only resolves when a package happens to share its part's name.
The wiki side is fixed and on prod: each part now gets its own package, and the recipe sends the real package name. This PR makes the bridge catch the same failure from any caller.
Changes
rust/lbr/src/lib.rs: new preflight check,package3d-refs-resolve. It fails when a<packageinstance>names a package the library does not have, and it names the offenders. It only runs on libraries with bindings, so existing golden results do not change.rust/bridge/src/orchestrations.rs:open_lbrtreats that check as a hard failure, so the file never reaches Fusion and no modal appears. It also runsapply_failure_dialogsbefore returning, so any other refusal comes back assuccess: falseand the Error dialog is closed.rust/bridge/src/mint.rs:build_library_3d_corekeeps the open's error, and reportssuccess: falsewhen the library will not open, even if every part minted.
Testing
cargo test -p fusion-lbr: 23 passed, including a new test with a dangling binding and a good one.cargo test -p adom-fusion-bridge: 113 passed.- Not run against real Fusion. The check was not exercised on the Windows box, and neither was the
apply_failure_dialogspath inopen_lbr.
For review
apply_failure_dialogscloses the Error dialog after recording it. The barefusion_open_lbrroute may already apply dialog handling, so please check that running it twice does no harm there.build_library_3d_corenow always includes anerrorkey, null on success.
Based on the wiki's HEAD at bridge version 1.13.69. No version bump, so you can release it with your next batch.
Generated with Claude Code
Diff Skip to comments
@@ -1,1238 +1,1264 @@⋯ 888 unchanged lines ⋯ } c("pads-unique", dups.is_empty(), if dups.is_empty() { "all pad names unique per package".into() } else { format!("duplicate pad names - Fusion's importer refuses the ENTIRE library over these: {}. Rename to e.g. 17, 17@2, 17@3...", dups.iter().take(6).cloned().collect::<Vec<_>>().join(", ")) });+ // A 3D package naming a package the library does not have makes Fusion refuse the ENTIRE+ // library: an Error modal and an empty design. Only libraries with bindings get this check.+ if root.descendants().any(|n| el(n, "packageinstance")) {+ let names: Vec<&str> = root.descendants().filter(|n| el(*n, "package")).filter_map(|p| p.attribute("name")).collect();+ let dangling: Vec<&str> = root.descendants().filter(|n| el(*n, "packageinstance")).filter_map(|p| p.attribute("name")).filter(|n| !names.iter().any(|k| k.eq_ignore_ascii_case(n))).collect();+ c("package3d-refs-resolve", dangling.is_empty(), if dangling.is_empty() { "every 3D binding names a package in this library".into() } else {+ format!("3D binding(s) name packages this library does not have, so Fusion refuses the ENTIRE library: {}. Bind to the <package> name, not the deviceset's.", dangling.iter().take(6).copied().collect::<Vec<_>>().join(", ")) });+ } } finish(checks, xml.as_deref()) }⋯ 19 unchanged lines ⋯ #[cfg(test)] mod heatsink_tests { #[test]+ fn preflight_refuses_a_3d_binding_to_a_missing_package() {+ let lib = |instance: &str| format!(r#"<?xml version="1.0"?><eagle><drawing><library><packages><package name="SOIC-8"><smd name="1" x="0" y="0" dx="1" dy="1" layer="1"/></package></packages><packages3d><package3d name="{instance}" wip_urn="urn:x"><packageinstances><packageinstance name="{instance}"/></packageinstances></package3d></packages3d><symbols><symbol name="S"/></symbols><devicesets><deviceset name="NY8A054ES8"><devices><device name="" package="SOIC-8"><connects/></device></devices></deviceset></devicesets></library></drawing></eagle>"#);+ let dir = std::env::temp_dir().join(format!("fusion-lbr-refs-{}", std::process::id()));+ std::fs::create_dir_all(&dir).unwrap();+ let check = |instance: &str| {+ let path = dir.join(format!("{instance}.lbr"));+ std::fs::write(&path, lib(instance)).unwrap();+ crate::preflight_lbr(&path).checks.into_iter().find(|c| c.check == "package3d-refs-resolve").expect("the check runs on a library with bindings")+ };+ let bad = check("NY8A054ES8");+ assert!(!bad.ok, "a binding to the deviceset's name is refused");+ assert!(bad.detail.contains("NY8A054ES8"), "and names it: {}", bad.detail);+ assert!(check("SOIC-8").ok, "a binding to the real package passes");+ let _ = std::fs::remove_dir_all(&dir);+ }++ #[test] fn heatsink_vias_are_not_shorts() { let lbr = r#"<eagle version="9.6.2"><drawing><library><packages><package name="QFN-EP"> <smd name="B4" x="0" y="0" dx="3" dy="3" layer="1"/>⋯ 316 unchanged lines ⋯ let text = root.to_xml_document(); Ok((text, json!({"changed": changed, "connectsDropped": dropped, "renamed": renames.iter().map(|(a, b)| json!({"from": a, "to": b})).collect::<Vec<_>>()}))) }+@@ -1,1803 +1,1806 @@⋯ 98 unchanged lines ⋯ "acceptedArgs": ["filePath", "lbrPath", "libraryPath", "path"], "receivedArgs": received, "_hint": "Re-call with a Windows-local path, e.g. fusion_open_lbr {\"lbrPath\": \"C:/tmp/rayparts/tja1050.lbr\"}."}); } let pf = fusion_lbr::preflight_lbr(std::path::Path::new(&file_path));- let hard_ok = ["file-readable", "size-sane", "xml-parses", "root-is-eagle"].iter().all(|k| pf.checks.iter().find(|c| c.check == *k).map(|c| c.ok).unwrap_or(true));+ let hard_ok = ["file-readable", "size-sane", "xml-parses", "root-is-eagle", "package3d-refs-resolve"].iter().all(|k| pf.checks.iter().find(|c| c.check == *k).map(|c| c.ok).unwrap_or(true)); let file_path = file_path.replace('\\', "/"); if !hard_ok { let sz = std::fs::metadata(&file_path).ok().map(|m| m.len());⋯ 58 unchanged lines ⋯ if !verification.is_null() { response["data"]["verification"] = verification; } let (shots, _) = crate::lifecycle::post_open_screenshot(ctx, 2.0); response["data"]["postOpenScreenshot"] = shots;- response+ // Fusion reports a refused library only through its Error modal: the command itself succeeds+ // and leaves an empty design. Without this, every caller reads the refusal as a good open.+ crate::small_verbs::apply_failure_dialogs(response, &ctx.caller, &ctx.pids) } /// Python's `repr(bytes)`.⋯ 1630 unchanged lines ⋯ assert!(seen > 3, "no boards found"); } }+@@ -1,649 +1,657 @@⋯ 366 unchanged lines ⋯ } } let mut opened = Value::Null;- if let Some(b) = &bound { if args["openWhenDone"].as_bool().unwrap_or(true) { opened = json!(crate::orchestrations::open_lbr(ctx, &json!({"filePath": b}), pass)["success"].as_bool() == Some(true)); } }+ let mut open_error = Value::Null;+ if let Some(b) = &bound { if args["openWhenDone"].as_bool().unwrap_or(true) {+ let op = crate::orchestrations::open_lbr(ctx, &json!({"filePath": b}), pass);+ let ok = op["success"].as_bool() == Some(true);+ opened = json!(ok);+ if !ok { open_error = op["error"].clone(); }+ } } let n_ok = results.iter().filter(|r| r["success"].as_bool() == Some(true)).count();- json!({"success": n_ok > 0, "cancelled": cancelled, "boundLbr": bound, "partsBound": n_ok, "partsTotal": total, "parts": results, "screenshots": shots, "opened": opened,+ // Parts minted but a library Fusion will not open is a failed build, whatever each part did.+ json!({"success": n_ok > 0 && open_error.is_null(), "error": open_error, "cancelled": cancelled, "boundLbr": bound, "partsBound": n_ok, "partsTotal": total, "parts": results, "screenshots": shots, "opened": opened, "_hint": format!("{n_ok}/{total} parts got a RENDERING footprint+chip 3D package; all bindings injected into {} and the library opened once. NEXT: fusion_check_dialogs should be 0 (no broken-ref). For clean device previews + screenshots, save to the cloud and REOPEN from the cloud (fusion_aps_open) - the in-session view often gets an empty 'Untitled' shoved in front. Each device's lower-right 3D preview should show footprint + chip (NOT 'Thumbnail download failed'). The BEFORE (footprint) / AFTER (chip placed) PNGs are in screenshots[] (on the box in %TEMP%/adom-fusion-shots; ab pulls them to you as localFullPath) - pull them with desktop_pull_file (or re-capture via desktop_screenshot_window) for the demo video. AFTER (recommended): call fusion_capture_library_views per showcase part to grab the symbol / footprint / component (pin<->pad mapped) views - those are what make EEs trust the library, and the 3D-only shots miss them. PITFALLS: needs Fusion running. ⚠️ RELAY TIMEOUT: a many-part run takes minutes but the Adom Bridge relay times out the REQUEST at ~60s - you may get 'Request timed out' even though the build KEEPS RUNNING server-side and finishes. Do NOT assume it failed: wait, then verify by reading the boundLbr (count <package3d name=) + the before/after PNGs (%TEMP%/adom-fusion-shots on the box, pulled to you as localFullPath). If a part is missing, just re-run build_library_3d for the missing parts pointing outLbrPath at the SAME file - binding injection is now MERGE-AWARE and idempotent, so it accumulates onto the existing bindings (it no longer wipes the parts that already succeeded). Each part also self-retries once on a transient first-part failure. Full recipe: the fusion-multipart-libraries skill.", bound.as_deref().unwrap_or("None"))}) } ⋯ 271 unchanged lines ⋯ assert!(q.contains(r#"_PN="Bob's \"project\"""#)); } }+
Reviews
Comments
Log in to comment.
No comments yet.