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
#135: whole library in the seed, checked ADDs, asserts reported
3 files changed
+71−7
CHANGELOG.md+14@@ -3,6 +3,20 @@ 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.45 (2026-10-31)++Issue #135 (Andy Engevold, AI Flow schematic_run: every ADD failed with "No part available", then Fusion's+PlacePartWorker assetAdded assert, clicked away, and the run went on past the caller's timeout):+- `fusion_board_seed` embeds the WHOLE project library in Seed.sch (every deviceset, symbol, package and 3D package),+ not only the fixed parts' devices. Every later ADD is then from a library the design holds; on Fusion 2706 USE of a+ .lbr on disk is accepted but ADD finds nothing, which is what failed. `wholeLibrary:false` keeps the old seed.+- `fusion_schematic_run` ADDs from the design-held library (USE of the .lbr on disk only when the design does not hold+ it), checks after each ADD that the part is on the sheet, and stops at the first that did not land: `add_failed`+ with `failedAt`, `part` and why. The reply names `libraryFrom` (design or use).+- Every Fusion assertion the bridge clicks OK on is recorded; a verb during which a non-harmless assertion was raised+ (anything but the routing pusher.cpp one) fails with `fusion_assert` and `fusionAsserts`, instead of reporting+ success.+ ## 1.13.44 (2026-10-30) - The import's needs_approval reply carries ab's own answer (`approvalAnswer`) and no longer claims a five-minute wait
rust/bridge/src/etiquette.rs+22@@ -15,6 +15,14 @@ use std::time::{Duration, Instant}; use fusion_core::ab::{AbClient, Caller, Loopback}; static BADGED: AtomicBool = AtomicBool::new(false);+/// Every Fusion assertion the bridge clicked OK on (the background sentinel included), with when:+/// a verb's reply names the ones raised while it ran (issue #135: a fatal PlacePartWorker assert was+/// clicked away silently and the run went on).+static ASSERTS_SEEN: std::sync::Mutex<Vec<(f64, String)>> = std::sync::Mutex::new(Vec::new());+fn now_secs() -> f64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0) }+/// The routing assertion Fusion raises when a LINE completes a net's last airwire (pusher.cpp+/// inLocator) is informational; any other one means the command did not do its job.+pub fn assert_is_harmless(body: &str) -> bool { body.to_lowercase().contains("pusher.cpp") } static BADGED_HWND: std::sync::Mutex<i64> = std::sync::Mutex::new(0); /// `set_driving_badge`: the Adom mark on Fusion's taskbar button while Adom drives it. pub fn set_badge(caller: &Caller, hwnd: i64) -> bool {@@ -150,6 +158,7 @@ pub fn dismiss_unexpected_condition(ab: &dyn AbClient, caller: &Caller, pids: &[ let body = ab.call("desktop_find_controls", json!({"hwnd": h}), caller, 10).ok().map(|r| { let dd = if r["data"].is_object() { r["data"].clone() } else { r }; dd["controls"].as_array().or(dd["matches"].as_array()).cloned().unwrap_or_default().iter().filter_map(|c| c["name"].as_str()).filter(|n| n.to_lowercase().contains("unexpected condition")).map(str::to_string).next().unwrap_or_default() }).unwrap_or_default(); let clicked = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12).map(|r| r["invoked"].as_bool().unwrap_or(false) || r["success"].as_bool().unwrap_or(false)).unwrap_or(false); eprintln!("[fusion-bridge] Fusion Electronics assertion dialog {}: {}", if clicked { "dismissed (OK)" } else { "seen, OK did not land" }, body.chars().take(160).collect::<String>());+ if let Ok(mut v) = ASSERTS_SEEN.lock() { v.push((now_secs(), body.clone())); if v.len() > 50 { v.remove(0); } } return Some(if body.is_empty() { "Fusion Electronics has detected an unexpected condition".into() } else { body }); } None@@ -289,6 +298,19 @@ pub fn post(state_dir: &Path, caller: &Caller, cmd: &str, args: &Value, mut res: // the assertion lands a moment AFTER the command returns: look for it a few times let mut found = None; for i in 0..4 { found = dismiss_unexpected_condition(&ab, caller, pids); if found.is_some() { break; } if i < 3 { std::thread::sleep(Duration::from_millis(300)); } }+ let _ = found.take();+ // every assertion clicked while this verb ran (the sentinel's too): a harmless routing one is+ // noted; any other one fails the verb (#135: PlacePartWorker assetAdded = the ADD did not land)+ let seen: Vec<String> = ASSERTS_SEEN.lock().map(|v| v.iter().filter(|(t, _)| *t >= since).map(|(_, b)| b.clone()).collect()).unwrap_or_default();+ let fatal: Vec<&String> = seen.iter().filter(|b| !assert_is_harmless(b)).collect();+ if !fatal.is_empty() && res.is_object() {+ res["success"] = json!(false);+ if res["errorCode"].is_null() || res["errorCode"] == json!("") { res["errorCode"] = json!("fusion_assert"); }+ res["fusionAsserts"] = json!(seen);+ let e = res["error"].as_str().unwrap_or("").to_string();+ res["error"] = json!(format!("Fusion raised an internal error during this verb ({}); the bridge clicked OK so Fusion is not blocked, but the command did not complete. {e}", fatal[0].chars().take(160).collect::<String>()).trim().to_string());+ }+ let found = seen.into_iter().last(); if let Some(body) = found { if res.is_object() { res["fusionUnexpectedCondition"] = json!(body); let h = res["_hint"].as_str().unwrap_or("").to_string(); res["_hint"] = json!(format!("{h} Fusion Electronics raised its 'unexpected condition' assertion during this verb and the bridge clicked OK; checkpoint the board now (fusion_export_eagle_source) and verify the copper with fusion_routing_state.").trim().to_string()); } } } }
rust/bridge/src/routing_script.py+35−7@@ -664,9 +664,20 @@ def sch_run(app, args): if m.group(2) != libname: fail('library_mismatch', 'ADD names library %s but libraryPath is %s' % (m.group(2), libname)) if m.group(1).upper() not in devices: fail('unknown_device', 'No deviceset %s in %s' % (m.group(1), lbr)) done = []- # (Fusion cannot take a library out of use from a command, so two in-use libraries with the same- # name make ADD ask which one; keep project library names unique in a session)- if lbr: app.executeTextCommand("Electron.run USE '%s';" % Path(lbr).as_posix())+ # ADD resolves from a library the design HOLDS (fusion_board_seed embeds the whole project library).+ # USE of the .lbr on disk is only the fallback when the design does not hold it: on Fusion 2706 that+ # USE is accepted but ADD then finds nothing (issue #135). (Fusion cannot take a library out of use+ # from a command, so two in-use libraries with the same name make ADD ask which one.)+ prod = app.activeProduct+ def part_names():+ try: return {prod.parts.item(i).name for i in range(prod.parts.count)}+ except Exception: return None+ held = set()+ try:+ for i in range(prod.libraries.count): held.add(prod.libraries.item(i).name)+ except Exception: pass+ used = 'design' if lbr and libname in held else ('use' if lbr else None)+ if used == 'use': app.executeTextCommand("Electron.run USE '%s';" % Path(lbr).as_posix()) pause = min(max(float(args.get('pauseMs', 0)), 0), 3000) / 1000.0 for c in cmds: c = c.strip()@@ -675,9 +686,21 @@ def sch_run(app, args): if c.startswith('VALUE '): c = 'SET CONFIRM YES; ' + c + ' SET CONFIRM OFF;' # ADD of a device set the design already holds in another version asks "Ok to update?": # the library on disk was just proven, so updating to it is the intent- if c.startswith('ADD '): c = 'SET CONFIRM YES; ' + c + ' SET CONFIRM OFF;'+ is_add = c.startswith('ADD ')+ if is_add: c = 'SET CONFIRM YES; ' + c + ' SET CONFIRM OFF;' app.executeTextCommand('Electron.run ' + c) done.append(c)+ if is_add:+ # the part must be on the sheet now: stop at the first ADD that did not land, naming it,+ # instead of running on (#135 retried for minutes past the caller's timeout)+ want = SCH_FORMS[0][1].match(c.replace('SET CONFIRM YES; ', '').replace(' SET CONFIRM OFF;', '').strip()).group(3)+ names = part_names()+ if names is not None and want not in names:+ return {'success': False, 'mutated': len(done) > 1, 'errorCode': 'add_failed', 'failedAt': len(done) - 1, 'failedCommand': c, 'part': want,+ 'libraryFrom': used, 'librariesHeld': sorted(held), 'commands': len(done), 'documentName': name,+ 'error': 'ADD of %s did not land: Fusion found no such device in %s (%s). The commands before it ran.' % (want, libname,+ 'the design holds that library, so the device is missing from it' if used == 'design' else 'the design does not hold that library and USE of the .lbr on disk does not make it addable on this Fusion'),+ '_hint': 'Seed the design with fusion_board_seed (it embeds the whole library), or add the part to the design\'s library; then run the rest of the commands.'} if pause: app.executeTextCommand('Electron.ImmediateRepaint /full') import adsk; adsk.doEvents(); time.sleep(pause)@@ -689,7 +712,7 @@ def sch_run(app, args): ok = not missing return {'success': ok, 'mutated': True, 'verified': ok, 'errorCode': None if ok else 'readback_mismatch', 'error': None if ok else 'parts not on the sheet after ADD: %s' % missing, 'documentName': name,- 'parts': after, 'added': [p for p in after if p not in before], 'nets': nets, 'commands': len(done)}+ 'parts': after, 'added': [p for p in after if p not in before], 'nets': nets, 'commands': len(done), 'libraryFrom': used} def board_bom(board, args):@@ -824,8 +847,13 @@ def board_seed(args): lib = root.find('drawing/library'); LIB = Path(lbr).stem; lib.set('name', LIB) fixed = args.get('fixed') or [] keep = {str(f.get('deviceset')).upper() for f in fixed}- for d in list(lib.find('devicesets')):- if d.get('name').upper() not in keep: lib.find('devicesets').remove(d)+ # the WHOLE library goes into the design by default: schematic_run ADDs every other part from the+ # library the design holds, because on Fusion 2706 USE of a .lbr on disk is accepted but ADD then+ # finds nothing (issue #135: every ADD failed, "No part available"). wholeLibrary:false keeps only+ # the fixed parts' devices, as before.+ if args.get('wholeLibrary') is False:+ for d in list(lib.find('devicesets')):+ if d.get('name').upper() not in keep: lib.find('devicesets').remove(d) used_sym = {g.get('symbol') for d in lib.iter('deviceset') for g in d.iter('gate')} used_pkg = {x.get('package') for d in lib.iter('deviceset') for x in d.iter('device')} for x in list(lib.find('symbols')):