← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.53\ No newline at end of file+1.13.54\ No newline at end of file
CHANGELOG.md+10
@@ -3,6 +3,16 @@ 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.54 (2026-11-02)++- `fusion_sch_swap_part` works: REPLACE takes the part first, then the device, and the old order answered "Device not found",+  so the verb has done nothing since it shipped (only its dry run was tested). When the new device's prefix differs Fusion renames+  the part, so the name is put back, then `keepComment` runs. Where pins end elsewhere Fusion keeps the nets itself:+  `pinsJoined` reports them, and a wire joins any it lost.+- `fusion_bom` honours `dnp` and `excludeFromBom` from the board. `fusion_preferences` routes correctly. `fusion_select_items`+  checks each part exists (Fusion answers "Selected X" for any name). `fusion_update_pcb` does not report `executed` while+  parts on the sheet are missing from the board.+ ## 1.13.53 (2026-11-02)  Partials on the EDA Bridge Standard, schematic edits first, and three candidates from standard #29:
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.53",+  "version": "1.13.54",   "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.53"+version = "1.13.54" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.53"+version = "1.13.54" 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.53"+DASH_VERSION="1.13.54" 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.53",+  "version": "1.13.54",   "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.53",+  "version": "1.13.54",   "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+5−5
@@ -19,7 +19,7 @@ dependencies = [  [[package]] name = "adom-fusion-bridge"-version = "1.13.53"+version = "1.13.54" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.53"+version = "1.13.54" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.53"+version = "1.13.54" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.53"+version = "1.13.54" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.53"+version = "1.13.54" 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.53"+version = "1.13.54" edition = "2021" license = "MIT" 
rust/bridge/src/main.rs+8−1
@@ -117,6 +117,13 @@ fn standard_adapter(verb: &str, mut parsed: Value) -> (String, Value) {     // Altium's own machinery: one refusal verb answers them all, with what Fusion does instead     if unsupported::note(bare).is_some() { parsed["args"] = json!({"verb": bare}); parsed["command"] = json!("fusion_not_supported"); return ("fusion_not_supported".into(), parsed); }     match bare {+        // read, or with set change: fusion_get_preferences and fusion_set_preference as one verb+        "preferences" => {+            match a["set"].as_object().filter(|m| !m.is_empty()).cloned() {+                Some(m) => { parsed["args"] = Value::Object(m); parsed["command"] = json!("fusion_set_preference"); ("fusion_set_preference".to_string(), parsed) }+                None => { parsed["args"] = json!({}); parsed["command"] = json!("fusion_get_preferences"); ("fusion_get_preferences".to_string(), parsed) }+            }+        }         // the shared writer for the families it makes (no Fusion needed); Fusion's generator for the rest         "generate_body" if body::engine_makes(&a) => ("fusion_generate_body_engine".into(), parsed),         "generate_body" => {@@ -567,7 +574,7 @@ fn main() {                         r["shell"] = json!("rust"); r },                     "fusion_start" | "start" | "launch" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); 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: sd.clone(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, "start", &caller); let mut r = launch::start(&ctx, &parsed["args"]); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, "start", t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     "fusion_open_lbr" | "open_lbr" if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, "open_lbr", &caller); let mut r = orchestrations::open_lbr(&ctx, &parsed["args"], &|v, a| addin::pass_through(v, &a, &ctx)); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, "open_lbr", t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },-                    v @ ("fusion_show_symbol" | "show_symbol" | "fusion_show_footprint" | "show_footprint" | "fusion_show_library" | "show_library" | "fusion_show_project" | "show_project" | "fusion_show_schematic" | "show_schematic" | "fusion_show_2d_board" | "show_2d_board" | "fusion_show_3d_board" | "show_3d_board" | "fusion_capture_library_views" | "capture_library_views" | "fusion_library_views" | "library_views" | "fusion_open_cloud_file" | "open_cloud_file" | "fusion_save_lbr" | "save_lbr" | "fusion_cleanup_cloud_files" | "cleanup_cloud_files" | "fusion_inspect_bodies" | "inspect_bodies" | "fusion_board_stackup" | "board_stackup" | "fusion_export_optimized_glb" | "export_optimized_glb" | "fusion_export_3d" | "export_3d" | "fusion_board_summary" | "board_summary" | "fusion_read_schematic" | "read_schematic" | "fusion_export_pos" | "export_pos" | "fusion_render_board_layers" | "render_board_layers" | "fusion_bom" | "bom" | "fusion_export_manufacturing" | "export_manufacturing" | "fusion_update_pcb" | "update_pcb" | "fusion_preferences" | "preferences" | "fusion_select_items" | "select_items" | "fusion_pcb_report" | "pcb_report" | "fusion_import_step" | "import_step" | "fusion_fetch_optimized_glb" | "fetch_optimized_glb" | "fusion_set_design_rules" | "set_design_rules" | "fusion_make_test_project" | "make_test_project" | "fusion_project_create" | "project_create" | "fusion_test_board" | "test_board" | "fusion_build_board" | "build_board" | "fusion_swap_footprint" | "swap_footprint" | "fusion_panelize" | "panelize" | "fusion_make_showcase_project" | "make_showcase_project" | "fusion_open_design" | "open_design" | "fusion_new_electronics_from_eagle" | "new_electronics_from_eagle" | "fusion_aps_open" | "aps_open" | "fusion_schematic" | "schematic" | "fusion_eagle_import_status" | "eagle_import_status" | "fusion_show_attributes" | "show_attributes") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, cmd, &caller); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "show_symbol" => orchestrations::show_symbol(&ctx, &parsed["args"], &pass), "show_footprint" => orchestrations::show_footprint(&ctx, &parsed["args"], &pass), "show_library" => orchestrations::show_library(&ctx, &parsed["args"], &pass), "show_project" => match orchestrations::show_project(&ctx, &parsed["args"], &pass) { Some(r) => r, None => side("show_project", parsed["args"].clone(), 240) }, "capture_library_views" => small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &parsed["args"], &pass), &caller, &pids), "library_views" => { let mut r = small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &docs::library_views_args(&parsed["args"]), &pass), &caller, &pids); r["aliasOf"] = json!("fusion_capture_library_views"); r }, "open_cloud_file" => orchestrations::open_cloud_file(&ctx, &parsed["args"], &pass), "save_lbr" => small_verbs::merge_dialog_array(orchestrations::save_lbr(&ctx, &parsed["args"], &pass), &caller, &pids), "cleanup_cloud_files" => orchestrations::cleanup_cloud_files(&parsed["args"], &pass), "inspect_bodies" => orchestrations::inspect_bodies(&parsed["args"], &pass), "board_stackup" => services::board_stackup(&parsed["args"], &pass), "board_summary" => stdshapes::board_summary(&parsed["args"], &pass), "read_schematic" => stdshapes::read_schematic(&parsed["args"], &pass), "export_pos" => stdshapes::export_pos(&parsed["args"], &pass), "render_board_layers" => stdshapes::render_board_layers(&parsed["args"], &pass), "bom" => stdshapes::bom(&parsed["args"], &pass), "export_manufacturing" => stdshapes::export_manufacturing(&parsed["args"], &pass), "pcb_report" => stdshapes::pcb_report(&parsed["args"], &pass), "update_pcb" => stdshapes::update_pcb(&parsed["args"], &pass), "preferences" => stdshapes::preferences(&parsed["args"], &pass), "select_items" => stdshapes::select_items(&parsed["args"], &pass), "export_3d" => services::export_3d(&parsed["args"], &pass), "import_step" => if parsed["args"]["into"].as_str() == Some("new") { pass("import_step", parsed["args"].clone()) } else { services::import_step_with_board(&parsed["args"], &pass) }, "export_optimized_glb" => if parsed["args"]["async"].as_bool() == Some(true) { services::start_glb_export_async(&ctx, &parsed["args"]) } else { services::export_optimized_glb(&parsed["args"], &pass) }, "fetch_optimized_glb" => if parsed["args"]["exportId"].is_string() { services::glb_export_async_status(&parsed["args"]) } else { services::fetch_optimized_glb(&parsed["args"]) }, "set_design_rules" => services::set_design_rules(&ctx, &parsed["args"], &pass), "make_test_project" => small_verbs::merge_dialog_array(testproject::make_test_project(&ctx, &parsed["args"], &pass), &caller, &pids), "project_create" => small_verbs::merge_dialog_array(testproject::project_create(&ctx, &parsed["args"], &pass), &caller, &pids), "test_board" => small_verbs::merge_dialog_array(boardgen::test_board(&ctx, &parsed["args"], &pass), &caller, &pids), "build_board" => small_verbs::merge_dialog_array(boardgen::build_board(&ctx, &parsed["args"], &pass), &caller, &pids), "swap_footprint" => small_verbs::merge_dialog_array(boardgen::swap_footprint(&ctx, &parsed["args"], &pass), &caller, &pids), "panelize" => small_verbs::merge_dialog_array(boardgen::panelize(&ctx, &parsed["args"], &pass), &caller, &pids), "make_showcase_project" => small_verbs::merge_dialog_array(testproject::make_showcase_project(&ctx, &parsed["args"], &pass), &caller, &pids), "open_design" => orchestrations::open_design(&ctx, &parsed["args"], &pass), "new_electronics_from_eagle" => orchestrations::new_electronics_from_eagle(&ctx, &parsed["args"], &pass), "schematic" => orchestrations::schematic(&ctx, &parsed["args"], &pass), "eagle_import_status" => orchestrations::eagle_import_status(&ctx, &parsed["args"]), "show_attributes" => orchestrations::show_attributes(&ctx, &parsed["args"], &pass), "aps_open" => orchestrations::aps_open(&ctx, &parsed["args"], &pass), other => orchestrations::open_with_screenshot(&ctx, other, &parsed["args"], &pass) }; let mut r = small_verbs::merge_dialog_array(r0, &caller, &pids); small_verbs::add_image_delivery(&mut r); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },+                    v @ ("fusion_show_symbol" | "show_symbol" | "fusion_show_footprint" | "show_footprint" | "fusion_show_library" | "show_library" | "fusion_show_project" | "show_project" | "fusion_show_schematic" | "show_schematic" | "fusion_show_2d_board" | "show_2d_board" | "fusion_show_3d_board" | "show_3d_board" | "fusion_capture_library_views" | "capture_library_views" | "fusion_library_views" | "library_views" | "fusion_open_cloud_file" | "open_cloud_file" | "fusion_save_lbr" | "save_lbr" | "fusion_cleanup_cloud_files" | "cleanup_cloud_files" | "fusion_inspect_bodies" | "inspect_bodies" | "fusion_board_stackup" | "board_stackup" | "fusion_export_optimized_glb" | "export_optimized_glb" | "fusion_export_3d" | "export_3d" | "fusion_board_summary" | "board_summary" | "fusion_read_schematic" | "read_schematic" | "fusion_export_pos" | "export_pos" | "fusion_render_board_layers" | "render_board_layers" | "fusion_bom" | "bom" | "fusion_export_manufacturing" | "export_manufacturing" | "fusion_update_pcb" | "update_pcb" | "fusion_select_items" | "select_items" | "fusion_pcb_report" | "pcb_report" | "fusion_import_step" | "import_step" | "fusion_fetch_optimized_glb" | "fetch_optimized_glb" | "fusion_set_design_rules" | "set_design_rules" | "fusion_make_test_project" | "make_test_project" | "fusion_project_create" | "project_create" | "fusion_test_board" | "test_board" | "fusion_build_board" | "build_board" | "fusion_swap_footprint" | "swap_footprint" | "fusion_panelize" | "panelize" | "fusion_make_showcase_project" | "make_showcase_project" | "fusion_open_design" | "open_design" | "fusion_new_electronics_from_eagle" | "new_electronics_from_eagle" | "fusion_aps_open" | "aps_open" | "fusion_schematic" | "schematic" | "fusion_eagle_import_status" | "eagle_import_status" | "fusion_show_attributes" | "show_attributes") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, cmd, &caller); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "show_symbol" => orchestrations::show_symbol(&ctx, &parsed["args"], &pass), "show_footprint" => orchestrations::show_footprint(&ctx, &parsed["args"], &pass), "show_library" => orchestrations::show_library(&ctx, &parsed["args"], &pass), "show_project" => match orchestrations::show_project(&ctx, &parsed["args"], &pass) { Some(r) => r, None => side("show_project", parsed["args"].clone(), 240) }, "capture_library_views" => small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &parsed["args"], &pass), &caller, &pids), "library_views" => { let mut r = small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &docs::library_views_args(&parsed["args"]), &pass), &caller, &pids); r["aliasOf"] = json!("fusion_capture_library_views"); r }, "open_cloud_file" => orchestrations::open_cloud_file(&ctx, &parsed["args"], &pass), "save_lbr" => small_verbs::merge_dialog_array(orchestrations::save_lbr(&ctx, &parsed["args"], &pass), &caller, &pids), "cleanup_cloud_files" => orchestrations::cleanup_cloud_files(&parsed["args"], &pass), "inspect_bodies" => orchestrations::inspect_bodies(&parsed["args"], &pass), "board_stackup" => services::board_stackup(&parsed["args"], &pass), "board_summary" => stdshapes::board_summary(&parsed["args"], &pass), "read_schematic" => stdshapes::read_schematic(&parsed["args"], &pass), "export_pos" => stdshapes::export_pos(&parsed["args"], &pass), "render_board_layers" => stdshapes::render_board_layers(&parsed["args"], &pass), "bom" => stdshapes::bom(&parsed["args"], &pass), "export_manufacturing" => stdshapes::export_manufacturing(&parsed["args"], &pass), "pcb_report" => stdshapes::pcb_report(&parsed["args"], &pass), "update_pcb" => stdshapes::update_pcb(&parsed["args"], &pass), "select_items" => stdshapes::select_items(&parsed["args"], &pass), "export_3d" => services::export_3d(&parsed["args"], &pass), "import_step" => if parsed["args"]["into"].as_str() == Some("new") { pass("import_step", parsed["args"].clone()) } else { services::import_step_with_board(&parsed["args"], &pass) }, "export_optimized_glb" => if parsed["args"]["async"].as_bool() == Some(true) { services::start_glb_export_async(&ctx, &parsed["args"]) } else { services::export_optimized_glb(&parsed["args"], &pass) }, "fetch_optimized_glb" => if parsed["args"]["exportId"].is_string() { services::glb_export_async_status(&parsed["args"]) } else { services::fetch_optimized_glb(&parsed["args"]) }, "set_design_rules" => services::set_design_rules(&ctx, &parsed["args"], &pass), "make_test_project" => small_verbs::merge_dialog_array(testproject::make_test_project(&ctx, &parsed["args"], &pass), &caller, &pids), "project_create" => small_verbs::merge_dialog_array(testproject::project_create(&ctx, &parsed["args"], &pass), &caller, &pids), "test_board" => small_verbs::merge_dialog_array(boardgen::test_board(&ctx, &parsed["args"], &pass), &caller, &pids), "build_board" => small_verbs::merge_dialog_array(boardgen::build_board(&ctx, &parsed["args"], &pass), &caller, &pids), "swap_footprint" => small_verbs::merge_dialog_array(boardgen::swap_footprint(&ctx, &parsed["args"], &pass), &caller, &pids), "panelize" => small_verbs::merge_dialog_array(boardgen::panelize(&ctx, &parsed["args"], &pass), &caller, &pids), "make_showcase_project" => small_verbs::merge_dialog_array(testproject::make_showcase_project(&ctx, &parsed["args"], &pass), &caller, &pids), "open_design" => orchestrations::open_design(&ctx, &parsed["args"], &pass), "new_electronics_from_eagle" => orchestrations::new_electronics_from_eagle(&ctx, &parsed["args"], &pass), "schematic" => orchestrations::schematic(&ctx, &parsed["args"], &pass), "eagle_import_status" => orchestrations::eagle_import_status(&ctx, &parsed["args"]), "show_attributes" => orchestrations::show_attributes(&ctx, &parsed["args"], &pass), "aps_open" => orchestrations::aps_open(&ctx, &parsed["args"], &pass), other => orchestrations::open_with_screenshot(&ctx, other, &parsed["args"], &pass) }; let mut r = small_verbs::merge_dialog_array(r0, &caller, &pids); small_verbs::add_image_delivery(&mut r); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     "fusion_state" | "state" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); 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 mut r = orchestrations::fusion_state(&ctx, &parsed["args"]); r["shell"] = json!("rust"); r },                     v @ ("fusion_make_3d_package" | "make_3d_package" | "fusion_build_library_3d" | "build_library_3d" | "fusion_bind_library_3d" | "bind_library_3d" | "fusion_prove_library" | "prove_library" | "fusion_attach_3d_package" | "attach_3d_package" | "fusion_show_3d_package" | "show_3d_package" | "fusion_import_part" | "import_part") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); *LONG_OP.lock().unwrap() = Some(cmd.to_string()); *LONG_OP_SINCE.lock().unwrap() = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "make_3d_package" => mint::make_3d_package(&ctx, &parsed["args"], &pass), "bind_library_3d" => mint::bind_library_3d(&ctx, &parsed["args"], &pass), "prove_library" => prove::prove_library(&ctx, &parsed["args"], &pass), "attach_3d_package" => mint::attach_3d_package(&ctx, &parsed["args"], &pass), "show_3d_package" => mint::show_3d_package(&ctx, &parsed["args"], &pass), "import_part" => partimport::import_part(&ctx, &parsed["args"], &pass), _ => mint::build_library_3d(&ctx, &parsed["args"], &pass) }; *LONG_OP.lock().unwrap() = None; let mut r = small_verbs::apply_failure_dialogs(r0, &caller, &pids); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     v @ ("fusion_apply_fusion_update" | "apply_fusion_update" | "fusion_tool_update" | "tool_update" | "fusion_window_info" | "window_info" | "fusion_install_fusion" | "install_fusion" | "fusion_uninstall" | "uninstall" | "fusion_prefs_open" | "prefs_open" | "fusion_prefs_close" | "prefs_close" | "fusion_generate_package" | "generate_package" | "fusion_mcp_status" | "mcp_status" | "fusion_mcp_tools" | "mcp_tools" | "fusion_mcp_call" | "mcp_call" | "fusion_mcp_resources" | "mcp_resources" | "fusion_mcp_enable" | "mcp_enable" | "fusion_library_manager" | "library_manager" | "fusion_managed_library_create" | "managed_library_create" | "fusion_managed_library_link" | "managed_library_link" | "fusion_managed_library_pull" | "managed_library_pull" | "fusion_managed_library_push" | "managed_library_push" | "fusion_managed_library_unlink" | "managed_library_unlink" | "fusion_signin" | "signin" | "fusion_signin_2fa" | "signin_2fa" | "fusion_web_signin" | "web_signin" | "fusion_aps_signin" | "aps_signin" | "fusion_demo" | "demo" | "fusion_demo_progress" | "demo_progress" | "fusion_demo_control" | "demo_control" | "fusion_electronics_query" | "electronics_query" | "fusion_view" | "view" | "fusion_interactive" | "interactive" | "fusion_narrate" | "narrate") if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); 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 pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let cmd = v.strip_prefix("fusion_").unwrap_or(v); let mut r = match cmd { "apply_fusion_update" => maintenance::apply_fusion_update(&ctx, &pass), "tool_update" => maintenance::tool_update(&ctx, &parsed["args"], &pass), "window_info" => stdshapes::window_info(&ctx), "install_fusion" => maintenance::install_fusion(&ctx), "prefs_open" => maintenance::prefs_open(&ctx, &parsed["args"], &pass), "prefs_close" => maintenance::prefs_close(&ctx, &parsed["args"]), "generate_package" => epg::generate_package(&parsed["args"], VERSION, &pass),
rust/bridge/src/sch_script.py+13−5
@@ -1149,15 +1149,22 @@ def sch_swap_part(app, args):         return out     before = by_pad(e.root, e.sheet)     ends_before = ends(e.root, e.sheet)-    e.cmds.append("SET CONFIRM YES; REPLACE '%s@%s' %s; SET CONFIRM OFF;" % (full, libname, quote(ref)))-    if args.get('keepComment') and old_value:-        e.cmds.append('SET CONFIRM YES; VALUE %s %s; SET CONFIRM OFF;' % (quote(ref), quote(old_value[:40])))+    # REPLACE takes the part first, then the device (the other order answers "Device not found"). Fusion names+    # the part afresh when the new device's prefix differs; the name is put back after the run.+    e.cmds.append("SET CONFIRM YES; REPLACE %s '%s@%s'; SET CONFIRM OFF;" % (quote(ref), full, libname))     if dry(args):         return plan_reply(e, was={'deviceset': part.get('deviceset'), 'device': part.get('device'), 'library': part.get('library')})     if lpath:         try: use_library(app, lpath)         except RuntimeError as ex: sfail('library_not_writable', str(ex).split(': ', 1)[-1])     e.run()+    names_before = {p_.get('name') for p_ in e.root.iter('part')}+    if not any(p_.get('name') == ref for p_ in e.after.iter('part')):+        fresh = [p_.get('name') for p_ in e.after.iter('part') if p_.get('name') not in names_before]+        if len(fresh) == 1:+            e.again(["SET CONFIRM YES; NAME %s %s; SET CONFIRM OFF;" % (quote(fresh[0]), quote(ref))])+    if args.get('keepComment') and old_value:   # after the name is back+        e.again(['SET CONFIRM YES; VALUE %s %s; SET CONFIRM OFF;' % (quote(ref), quote(old_value[:40]))])     now_part = next((p_ for p_ in e.after.iter('part') if p_.get('name') == ref), None)     if lpath and now_part is not None and now_part.get('library') == part.get('library') and now_part.get('deviceset') == part.get('deviceset') and now_part.get('device') == part.get('device'):         sfail('library_not_loaded', _not_loaded(lpath))@@ -1168,12 +1175,13 @@ def sch_swap_part(app, args):     # where a new pin ends elsewhere, a wire joins it to where the old pin ended (matched by pad number)     ends_after = ends(e.after, e.sheet2)     join = [p_ for p_ in lost if p_ in ends_after and ends_after[p_] != ends_before.get(p_)]-    joined = []     if join:         e.again(['SET CONFIRM YES; NET %s %s %s; SET CONFIRM OFF;' % (quote(before[p_]), mm(ends_before[p_]), mm(ends_after[p_])) for p_ in join])         after = by_pad(e.after, e.sheet2)-        joined = [{'pin': p_, 'from': list(ends_before[p_]), 'to': list(ends_after[p_])} for p_ in join if after.get(p_) == before[p_]]         lost = sorted(p_ for p_, n in before.items() if n and after.get(p_) != n)+    # every pin that ended elsewhere and kept its net: joined by Fusion's REPLACE, or by the wire above+    joined = [{'pin': p_, 'from': list(ends_before[p_]), 'to': list(ends_after[p_]), 'net': before[p_]}+              for p_ in sorted(before) if before[p_] and after.get(p_) == before[p_] and p_ in ends_after and ends_after[p_] != ends_before.get(p_)]     return e.reply(now={'libReference': '%s@%s' % (full, libname), 'footprints': [dev.get('package')] if dev is not None and dev.get('package') else []},                    oldPinsNotOnNew=sorted(set(before) - set(after)), newPinsNotOnOld=sorted(set(after) - set(before)),                    lostConnections=[{'pin': p_, 'net': before[p_]} for p_ in lost], pinsJoined=joined,
rust/bridge/src/stdshapes.rs+13−14
@@ -237,10 +237,10 @@ pub fn bom(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {         eargs["variant"] = json!({"name": v["name"], "notFitted": v["notFitted"],             "alternates": v["alternates"].as_array().cloned().unwrap_or_default().iter().map(|a| json!({"designator": a["designator"], "part": a["value"]})).collect::<Vec<_>>()});     }-    // edit_parts' DNP and EXCLUDE_FROM_BOM attributes count as not fitted, with or without a variant-    let marked: Vec<Value> = sd["components"].as_array().into_iter().flatten().filter(|c| ["DNP", "EXCLUDE_FROM_BOM"].iter().any(|k| {-        c["params"][*k].as_str().map(|v| matches!(v.to_lowercase().as_str(), "yes" | "true" | "1")).unwrap_or(false)-    })).map(|c| c["designator"].clone()).collect();+    // edit_parts' DNP and EXCLUDE_FROM_BOM attributes count as not fitted, with or without a variant. They live+    // on the board's parts, so board_bom says which (excluded)+    let bb = crate::routing::call("board_bom", &json!({"expectDocument": args["expectDocument"]}), pass);+    let marked = bb["excluded"].as_array().cloned().unwrap_or_default();     if !marked.is_empty() {         if eargs["variant"].is_null() { eargs["variant"] = json!({"name": "", "notFitted": [], "alternates": []}); }         let mut nf = eargs["variant"]["notFitted"].as_array().cloned().unwrap_or_default();@@ -430,18 +430,13 @@ pub fn update_pcb(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {     if execute && args["allowRemovals"].as_bool() == Some(true) && remove.as_array().map(|a| !a.is_empty()).unwrap_or(false) {         return json!({"success": false, "errorCode": "not_supported", "preview": {"addParts": add, "removeParts": remove}, "error": "Fusion cannot delete a part from a linked board by command: delete it in Fusion, or leave it (a part only on the board is usually a logo or a mounting hole).", "executed": false});     }-    json!({"success": true, "executed": execute, "changeOrder": [],+    // nothing to run when the two agree. Parts only on the sheet are either package-less (supply symbols,+    // frames) or a design whose board is not linked to its sheet: never call that done+    let behind = add.as_array().map(|a| !a.is_empty()).unwrap_or(false);+    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": "Fusion already keeps the board in step with the sheet, so there is nothing to execute. addParts are on the sheet with no package (supply symbols, frames); removeParts are only on the board and stay there."})-}--/// `fusion_preferences` (the standard's preferences): read Fusion's settings, or change them with set.-pub fn preferences(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {-    if let Some(set) = args["set"].as_object() {-        if !set.is_empty() { return pass("set_preference", Value::Object(set.clone())); }-    }-    pass("get_preferences", json!({}))+        "_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." }}) }  /// `fusion_select_items` (the standard's select_items): select parts by designator, or clear the selection.@@ -453,8 +448,12 @@ pub fn select_items(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value     }     let refs: Vec<String> = args["refs"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();     if refs.is_empty() { return json!({"success": false, "errorCode": "bad_args", "error": "refs is [designator], or clear:true"}); }+    // Fusion answers "Selected X" for any name at all, so the names are checked against the editor in front+    let script = "p=app.activeProduct\nitems=getattr(p,'parts',None) or getattr(p,'elements',None)\nresult={'names':[items.item(i).name for i in range(items.count)] if items else None}";+    let known: Option<Vec<String>> = pass("run_modeling_script", json!({"script": script}))["data"]["result"]["names"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_uppercase)).collect());     let (mut selected, mut failed) = (Vec::new(), Vec::new());     for r in &refs {+        if let Some(k) = &known { if !k.contains(&r.to_uppercase()) { failed.push(json!({"ref": r, "error": "no such part in the editor in front"})); continue; } }         let x = pass("electron_select", json!({"name": r, "properties": false}));         if x["success"].as_bool() == Some(false) { failed.push(json!({"ref": r, "error": x["error"]})); } else { selected.push(json!(r)); }     }