← Commit history

five partials answered in the standard's shapes

Colby Knox ·f72addfc9d ·12d ago ·parent 1a2298e
7 files changed +297−11
CHANGELOG.md+9−1
@@ -3,7 +3,15 @@ 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.34 (unreleased)+## 1.13.34 (2026-10-30)++Partial standard verbs answered exactly, in the standard's own shapes (stdshapes.rs):+- `fusion_board_summary`: the standard's summary shaped from one read_board (it was fusion_board_info's reply).+- `fusion_read_schematic`: components with designator, libReference, comment and parameters, from schematic_data.+- `fusion_list_library`: a .lbr's component (sch) or footprint (pcb) names, offline.+- `fusion_export_pos`: pick-and-place per side, csv or ascii, mm or in, smdOnly, DNP left out, JLCPCB's CPL with+  fab:'jlcpcb'; format gerber refused by name (Fusion writes none).+- `fusion_window_info`: Fusion's windows and open dialogs without screenshots.  - `fusion_import_step` names the placed component after the import (Fusion's import renamed it, seen live as   Component376). Checked live on 1.13.33: a SOT-23-5 STEP placed in lane-BQ25792-copy's 3D PCB at (10, 10, 5) mm, turned
bridge.json+4
@@ -142,6 +142,7 @@     "fusion_export_odb",     "fusion_export_optimized_glb",     "fusion_export_pdf",+    "fusion_export_pos",     "fusion_export_sat",     "fusion_export_skp",     "fusion_export_source",@@ -191,6 +192,7 @@     "fusion_list_commands",     "fusion_list_designators",     "fusion_list_installed_libraries",+    "fusion_list_library",     "fusion_list_nets",     "fusion_list_text_commands",     "fusion_live_feed",@@ -255,6 +257,7 @@     "fusion_read_board",     "fusion_read_footprint",     "fusion_read_rules",+    "fusion_read_schematic",     "fusion_read_sheet_file",     "fusion_read_symbol",     "fusion_readiness",@@ -377,6 +380,7 @@     "fusion_view",     "fusion_walk_cloud_tree",     "fusion_web_signin",+    "fusion_window_info",     "fusion_xcopy_footprint",     "fusion_zoom_to"   ],
contracts/describe.json+88−4
@@ -915,16 +915,18 @@     {       "example": {},       "input": {-        "...": "as fusion_board_info"+        "expectDocument": "the board's name",+        "limit": "int, default 2000 (components listed)",+        "raw": "bool"       },       "longRunning": false,       "name": "fusion_board_summary",       "output": {-        "...": "as fusion_board_info",-        "aliasOf": "fusion_board_info"+        "roundtripMs": "int",+        "summary": "{board, components, componentsTop, componentsBottom, nets, copperLayersInUse, copperLayersInUseNames, layersWithPrimitives, copperLayersAddressable, rules, rulesEnabled}"       },       "statusVerb": null,-      "summary": "The EDA Bridge Standard's name for fusion_board_info: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+      "summary": "One-screen summary of the open board (the standard's board_summary), shaped from one fusion_read_board round trip: the board (name, outline), its components with footprint, value, position and side, counts top and bottom, nets, the copper layers in use and addressable, layers carrying primitives, and the rule counts. raw:true answers read_board unshaped.",       "timeoutSeconds": 120     },     {@@ -2604,6 +2606,32 @@       "summary": "Every sheet of the design as one PDF (the standard's export_pdf), each page Fusion's own render of the sheet; board:true adds the board as a second PDF. Fusion has no assembly or fabrication print outputs.",       "timeoutSeconds": 180     },+    {+      "example": {+        "fab": "jlcpcb",+        "side": "both"+      },+      "input": {+        "excludeDnp": "bool, default true",+        "expectDocument": "the board's name",+        "fab": "jlcpcb",+        "format": "csv (default) | ascii | gerber (refused)",+        "outputDir": "folder (default %TEMP%/adom-pos)",+        "side": "both (default) | front | back",+        "smdOnly": "bool",+        "units": "mm (default) | in"+      },+      "longRunning": false,+      "name": "fusion_export_pos",+      "output": {+        "dnpLeftOut": "[ref]",+        "files": "[path]",+        "parts": "{front, back}: int per side"+      },+      "statusVerb": null,+      "summary": "The board's pick-and-place file (the standard's export_pos) from its own parts and pads: csv or ascii, mm or in, front, back or both (one file per side), smdOnly (a part with any surface pad), DNP parts of the current variant left out (excludeDnp, default true), and fab:'jlcpcb' for JLCPCB's CPL columns too. Fusion writes no Gerber position file, so format gerber is refused by name.",+      "timeoutSeconds": 120+    },     {       "example": {         "outputPath": "C:/out/part.sat"@@ -3670,6 +3698,28 @@       "summary": "The libraries Fusion offers to place from (the standard's list_installed_libraries): the hub's electronics libraries (.flbr files), which Library Manager's Hub Libraries tab shows and fusion_install_library_bundle saves to. Fusion keeps no local list and draws that tab without UI Automation, so this walks the hub's projects and folders through the signed-in session, breadth first to a depth and within a time budget; complete:false when cut short.",       "timeoutSeconds": 300     },+    {+      "example": {+        "kind": "pcb",+        "libraryPath": "C:/libs/parts.lbr"+      },+      "input": {+        "kind": "sch (default) | pcb",+        "libraryPath": "the .lbr on the Fusion machine (alias filePath; required)",+        "limit": "int, default 500"+      },+      "longRunning": false,+      "name": "fusion_list_library",+      "output": {+        "data": "{kind, count, names}",+        "kind": "sch | pcb",+        "path": "str",+        "roundtripMs": "int"+      },+      "statusVerb": null,+      "summary": "Names in an EAGLE .lbr, offline (the standard's list_library): kind sch (the components, default) or pcb (the footprints); an EAGLE library holds both.",+      "timeoutSeconds": 30+    },     {       "example": {         "expectDocument": "fusion-routing"@@ -20374,6 +20424,26 @@       "summary": "Every design rule on the live board from Fusion's rule manager, disabled ones included: name, kind (Copper Clearance, Minimum Copper Width, Teardrop...), enabled, priority, both scopes, value and preferred value, whether live and manual DRC check its kind, plus the classic design-rule values (params) and the rule set's name.",       "timeoutSeconds": 60     },+    {+      "example": {},+      "input": {+        "expectDocument": "the design's name",+        "limit": "int, default 500",+        "params": "bool, default true"+      },+      "longRunning": false,+      "name": "fusion_read_schematic",+      "output": {+        "components": "[{designator, libReference, comment, device, library, parameters}]",+        "count": "int",+        "designators": "[str]",+        "document": "str",+        "roundtripMs": "int"+      },+      "statusVerb": null,+      "summary": "Components of the open design's schematic (the standard's read_schematic), from fusion_schematic_data: designator, libReference (the deviceset), comment (the value), device, library and the part's parameters.",+      "timeoutSeconds": 120+    },     {       "example": {         "path": "C:/Users/me/Documents/board.sch"@@ -23278,6 +23348,20 @@       "summary": "Sign a pup browser window into an Autodesk WEB surface (library.io, the hub portal at <hub>.autodesk360.com, Autodesk Account) via Google SSO. Fusion's DESKTOP app being signed in does NOT sign these in - they are separate web sessions. NEVER guesses a credential: a missing password or a 2FA challenge comes back as a typed stop (human_credential_required / second_factor_required / google_sso_unavailable) naming the one-time command the USER runs. The password never passes through this bridge or the AI - it is stored once with ab's credential_set and filled by pup in the background.",       "timeoutSeconds": 240     },+    {+      "example": {},+      "input": {},+      "longRunning": false,+      "name": "fusion_window_info",+      "output": {+        "dialogs": "[{hwnd, title, body, buttons, category, safeAction, ownedByMainInstance}]",+        "hasModalDialogs": "bool",+        "windows": "[{hwnd, title, kind, className, pid, rect}]"+      },+      "statusVerb": null,+      "summary": "Fusion's top-level windows and its open dialogs, without screenshots (the standard's window_info): each window's hwnd, title, kind, class, pid and rect, and each dialog's title, text, buttons, category and safe action.",+      "timeoutSeconds": 60+    },     {       "example": {},       "input": {
describe.py+10−2
@@ -1111,8 +1111,16 @@ _T = [      {"...":"as fusion_export_gerbers"}, {"aliasOf":"fusion_export_gerbers","...":"as fusion_export_gerbers"}, 120, None, False, {}),     ("fusion_dismiss_dialogs", "The EDA Bridge Standard's name for fusion_dismiss_blocking_dialogs: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",      {"...":"as fusion_dismiss_blocking_dialogs"}, {"aliasOf":"fusion_dismiss_blocking_dialogs","...":"as fusion_dismiss_blocking_dialogs"}, 120, None, False, {}),-    ("fusion_board_summary", "The EDA Bridge Standard's name for fusion_board_info: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",-     {"...":"as fusion_board_info"}, {"aliasOf":"fusion_board_info","...":"as fusion_board_info"}, 120, None, False, {}),+    ("fusion_board_summary", "One-screen summary of the open board (the standard's board_summary), shaped from one fusion_read_board round trip: the board (name, outline), its components with footprint, value, position and side, counts top and bottom, nets, the copper layers in use and addressable, layers carrying primitives, and the rule counts. raw:true answers read_board unshaped.",+     {"expectDocument": "the board's name", "limit": "int, default 2000 (components listed)", "raw": "bool"}, {"summary": "{board, components, componentsTop, componentsBottom, nets, copperLayersInUse, copperLayersInUseNames, layersWithPrimitives, copperLayersAddressable, rules, rulesEnabled}", "roundtripMs": "int"}, 120, None, False, {}),+    ("fusion_read_schematic", "Components of the open design's schematic (the standard's read_schematic), from fusion_schematic_data: designator, libReference (the deviceset), comment (the value), device, library and the part's parameters.",+     {"expectDocument": "the design's name", "params": "bool, default true", "limit": "int, default 500"}, {"components": "[{designator, libReference, comment, device, library, parameters}]", "count": "int", "designators": "[str]", "document": "str", "roundtripMs": "int"}, 120, None, False, {}),+    ("fusion_list_library", "Names in an EAGLE .lbr, offline (the standard's list_library): kind sch (the components, default) or pcb (the footprints); an EAGLE library holds both.",+     {"libraryPath": "the .lbr on the Fusion machine (alias filePath; required)", "kind": "sch (default) | pcb", "limit": "int, default 500"}, {"data": "{kind, count, names}", "kind": "sch | pcb", "path": "str", "roundtripMs": "int"}, 30, None, False, {"libraryPath": "C:/libs/parts.lbr", "kind": "pcb"}),+    ("fusion_export_pos", "The board's pick-and-place file (the standard's export_pos) from its own parts and pads: csv or ascii, mm or in, front, back or both (one file per side), smdOnly (a part with any surface pad), DNP parts of the current variant left out (excludeDnp, default true), and fab:'jlcpcb' for JLCPCB's CPL columns too. Fusion writes no Gerber position file, so format gerber is refused by name.",+     {"expectDocument": "the board's name", "side": "both (default) | front | back", "format": "csv (default) | ascii | gerber (refused)", "units": "mm (default) | in", "smdOnly": "bool", "excludeDnp": "bool, default true", "fab": "jlcpcb", "outputDir": "folder (default %TEMP%/adom-pos)"}, {"files": "[path]", "parts": "{front, back}: int per side", "dnpLeftOut": "[ref]"}, 120, None, False, {"side": "both", "fab": "jlcpcb"}),+    ("fusion_window_info", "Fusion's top-level windows and its open dialogs, without screenshots (the standard's window_info): each window's hwnd, title, kind, class, pid and rect, and each dialog's title, text, buttons, category and safe action.",+     {}, {"windows": "[{hwnd, title, kind, className, pid, rect}]", "dialogs": "[{hwnd, title, body, buttons, category, safeAction, ownedByMainInstance}]", "hasModalDialogs": "bool"}, 60, None, False, {}),     ("fusion_install", "The EDA Bridge Standard's name for fusion_install_fusion: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",      {"...":"as fusion_install_fusion"}, {"aliasOf":"fusion_install_fusion","...":"as fusion_install_fusion"}, 120, None, False, {}),     ("fusion_status", "The EDA Bridge Standard's name for fusion_readiness: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",
rust/bridge/src/main.rs+7−4
@@ -38,6 +38,7 @@ mod testproject; mod boardgen; mod partimport; mod unsupported;+mod stdshapes; mod replicate; mod appverbs; mod livefeed;@@ -76,7 +77,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); /// already does under its own verb: the standard name is answered by that verb, reply marked aliasOf. const STANDARD_ALIASES: &[(&str, &str)] = &[     ("launch", "fusion_start"), ("export_gerber", "fusion_export_gerbers"), ("dismiss_dialogs", "fusion_dismiss_blocking_dialogs"),-    ("board_summary", "fusion_board_info"), ("install", "fusion_install_fusion"),+    ("install", "fusion_install_fusion"),     ("status", "fusion_readiness"), ("save", "fusion_save_document"), ("run_script", "fusion_run_modeling_script"),     ("run_command", "fusion_execute_text_command"), ("list_commands", "fusion_list_text_commands"), ("dialog_log", "fusion_action_ledger"),     ("signin_status", "fusion_account"), ("license_status", "fusion_account"), ("open_document", "fusion_open_design"),@@ -566,10 +567,10 @@ 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_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), "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_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), "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_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), "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),+                    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),                             "signin" => { let readiness = || owned_readiness(&**h, &sidecar, &caller); let start = || launch::start(&ctx, &json!({})); let aps_state = || { let st = native_aps_status(&sidecar, &caller); (st["data"]["configured"].as_bool().unwrap_or(false), st["data"]["signedIn"].as_bool().unwrap_or(false)) }; let aps_signin = |p: &str| aps_signin::signin(&ctx, &json!({"profile": p})); signin::signin(&ctx, &parsed["args"], &signin::Env { readiness: &readiness, start: &start, aps_state: &aps_state, aps_signin: &aps_signin }) },                             "signin_2fa" => signin::signin_2fa(&ctx, &parsed["args"]), "aps_signin" => aps_signin::signin(&ctx, &parsed["args"]),                             "electronics_query" => electronics::query(&ctx, &parsed["args"], VERSION, &pass), "view" => tour::view(&pass, &parsed["args"]), "narrate" => tour::narrate(&parsed["args"]), "interactive" => tour::interactive(&pass, parsed["args"]["probeMainThread"].as_bool().unwrap_or(true)), "demo_progress" => if tour::owns(parsed["args"]["demoId"].as_str().unwrap_or("")) || parsed["args"]["demoId"].as_str().map(|s| s.is_empty()).unwrap_or(true) { tour::progress(&parsed["args"]) } else { demo::demo_progress(&ctx.state_dir, &parsed["args"]) },@@ -699,6 +700,7 @@ fn main() {                         | "fusion_clone_symbol" | "clone_symbol" | "fusion_rename_symbol" | "rename_symbol" | "fusion_remove_symbol" | "remove_symbol" | "fusion_set_params" | "set_params" | "fusion_footprint_from_land_pattern" | "footprint_from_land_pattern" | "fusion_set_models" | "set_models" | "fusion_add_models" | "add_models" | "fusion_symbol_prims" | "symbol_prims" | "fusion_set_visibility" | "set_visibility" | "fusion_add_symbol_art" | "add_symbol_art" | "fusion_lint_library" | "lint_library" | "fusion_plate_vias" | "plate_vias" | "fusion_cleanup_libs" | "cleanup_libs") => native_lib_authoring(v.strip_prefix("fusion_").unwrap_or(v), &parsed["args"]),                     "fusion_preflight_lbr" | "preflight_lbr" => native_preflight_lbr(&parsed["args"]),                     "fusion_not_supported" | "not_supported" => unsupported::answer(&parsed["args"]),+                    "fusion_list_library" | "list_library" => { let mut r = stdshapes::list_library(&parsed["args"]); r["shell"] = json!("rust"); r },                     "fusion_panel" | "panel" 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 mut r = appverbs::panel(&parsed["args"], &pass); r["shell"] = json!("rust"); r },                     // Native since Phase 2: the raw authenticated APS GET (never-charge guarded).                     "fusion_aps_get" | "aps_get" => native_aps_get(&parsed["args"]),@@ -1881,7 +1883,8 @@ mod standard_alias_tests {     #[test]     fn standard_names_answer_through_the_bridges_own_verbs() {         assert_eq!(super::standard_alias("fusion_launch"), Some("fusion_start"));-        assert_eq!(super::standard_alias("board_summary"), Some("fusion_board_info"));+        assert_eq!(super::standard_alias("save"), Some("fusion_save_document"));+        assert_eq!(super::standard_alias("board_summary"), None); // answered in its own shape (stdshapes)         assert_eq!(super::standard_alias("fusion_start"), None);         assert_eq!(super::standard_alias("fusion_routing_state"), None);         // every alias names a verb the frozen catalog serves
rust/bridge/src/stdshapes.rsadded+173
@@ -0,0 +1,173 @@+//! Standard verbs answered in the standard's own shape from what the bridge already reads: list_library+//! (a .lbr offline), board_summary (read_board), read_schematic (schematic_data), export_pos (the board's+//! parts and pads) and window_info (Fusion's windows and dialogs).++use serde_json::{json, Value};++use crate::addin::Ctx;++fn ms(t0: std::time::Instant) -> u64 { t0.elapsed().as_millis() as u64 }++/// `fusion_list_library`: the names in a .lbr. An EAGLE library holds both kinds, so `kind` picks:+/// sch (the components, default) or pcb (the footprints).+pub fn list_library(args: &Value) -> Value {+    let t0 = std::time::Instant::now();+    let Some(path) = args["libraryPath"].as_str().or(args["filePath"].as_str()).map(|p| p.replace('\\', "/")) else {+        return json!({"success": false, "errorCode": "bad_args", "error": "libraryPath is required: the .lbr on the Fusion machine (Fusion has no bundled default library)"});+    };+    let kind = match args["kind"].as_str().unwrap_or("sch") { "pcb" | "footprint" | "footprints" | "package" => "pcb", _ => "sch" };+    let limit = args["limit"].as_u64().unwrap_or(500) as usize;+    let text = match std::fs::read_to_string(&path) { Ok(t) => t, Err(e) => return json!({"success": false, "errorCode": "library_unreadable", "error": format!("cannot read {path}: {e}")}) };+    let root = match fusion_lbr::Element::parse(&text) { Ok(r) => r, Err(e) => return json!({"success": false, "errorCode": "library_unreadable", "error": e.to_string()}) };+    let (sec, tag) = if kind == "pcb" { ("packages", "package") } else { ("devicesets", "deviceset") };+    let mut names = Vec::new();+    fn walk<'a>(e: &'a fusion_lbr::Element, sec: &str, tag: &str, out: &mut Vec<String>) {+        if e.tag == sec { for c in e.children.iter().filter(|c| c.tag == tag) { if let Some(n) = c.attr("name") { out.push(n.to_string()); } } return; }+        for c in &e.children { walk(c, sec, tag, out); }+    }+    walk(&root, sec, tag, &mut names);+    let count = names.len();+    names.truncate(limit);+    json!({"success": true, "mutated": false, "data": {"kind": kind, "count": count, "names": names}, "kind": kind, "path": path, "roundtripMs": ms(t0),+           "_hint": if kind == "sch" { "Components (EAGLE devicesets). kind:'pcb' lists the footprints of the same library." } else { "Footprints (EAGLE packages). kind:'sch' lists the components." }})+}++/// `fusion_board_summary`: the standard's one-screen summary, shaped from one read_board round trip.+pub fn board_summary(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    let t0 = std::time::Instant::now();+    let rb = crate::routing::call("read_board", args, pass);+    if rb["success"].as_bool() != Some(true) { return rb; }+    if args["raw"].as_bool() == Some(true) { return rb; }+    let d = &rb["data"];+    let comps = d["components"]["components"].as_array().cloned().unwrap_or_default();+    let limit = args["limit"].as_u64().unwrap_or(2000) as usize;+    let side = |s: &str| comps.iter().filter(|c| c["side"] == s).count();+    let in_use = d["occupancy"]["copperLayersInUse"].as_array().cloned().unwrap_or_default();+    let rules = d["rules"]["rules"].as_array().cloned().unwrap_or_default();+    let enabled = rules.iter().filter(|r| r["enabled"].as_bool() != Some(false) && r["attrs"]["enabled"].as_str() != Some("no")).count();+    let summary = json!({+        "board": {"name": rb["documentName"], "boardId": rb["boardId"], "outline": d["outline"]["outline"], "outlineClosed": d["outline"]["outlineClosed"], "revision": rb["revision"]},+        "components": comps.iter().take(limit).map(|c| json!({"designator": c["ref"], "footprint": c["pattern"], "comment": c["value"], "x": c["x"], "y": c["y"], "rotation": c["rotation"], "side": c["side"], "locked": c["locked"]})).collect::<Vec<_>>(),+        "componentsTop": side("top"), "componentsBottom": side("bottom"),+        "nets": d["nets"]["nets"].as_array().map(|a| a.len()).unwrap_or(0),+        "copperLayersInUse": in_use.len(),+        "copperLayersInUseNames": in_use.iter().filter_map(|l| l["name"].as_str()).collect::<Vec<_>>(),+        "layersWithPrimitives": d["occupancy"]["layersWithPrimitives"],+        "copperLayersAddressable": d["layers"]["copperLayers"],+        "rules": rules.len(), "rulesEnabled": enabled,+    });+    json!({"success": true, "mutated": false, "summary": summary, "documentName": rb["documentName"], "revision": rb["revision"], "roundtripMs": ms(t0)})+}++/// `fusion_read_schematic`: the standard's components of the schematic, from schematic_data.+pub fn read_schematic(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    let t0 = std::time::Instant::now();+    let mut a = args.clone();+    if args["params"].as_bool() != Some(false) { a["params"] = json!(true); }+    let sd = crate::sch::call("schematic_data", &a, pass);+    if sd["success"].as_bool() == Some(false) { return sd; }+    let comps: Vec<Value> = sd["components"].as_array().cloned().unwrap_or_default().into_iter().map(|c| {+        let mut row = json!({"designator": c["designator"], "libReference": c["libReference"], "comment": c["comment"], "device": c["device"], "library": c["library"], "sheet": c["sheet"], "at": c["at"]});+        if c["params"].is_object() { row["parameters"] = c["params"].clone(); }+        row+    }).collect();+    let des: Vec<Value> = comps.iter().map(|c| c["designator"].clone()).collect();+    json!({"success": true, "mutated": false, "components": comps, "count": des.len(), "designators": des, "document": sd["documentName"], "truncated": sd["truncated"], "roundtripMs": ms(t0)})+}++/// One placed part as the pick-and-place file writes it.+struct Placed { refdes: String, value: String, footprint: String, x: f64, y: f64, rot: f64, top: bool, smd: bool }++/// `fusion_export_pos`: the pick-and-place file from the board's own parts and pads: csv or ascii, mm+/// or in, front/back/both, smdOnly, DNP parts left out (excludeDnp, default true), and fab:'jlcpcb'+/// for JLCPCB's CPL columns too. Fusion writes no Gerber position file, so that format is refused.+pub fn export_pos(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    let format = args["format"].as_str().unwrap_or("csv").to_lowercase();+    if format == "gerber" { return json!({"success": false, "errorCode": "not_supported", "error": "Fusion writes no Gerber position (X3) file: format csv or ascii"}); }+    if format != "csv" && format != "ascii" { return json!({"success": false, "errorCode": "bad_args", "error": format!("format is csv, ascii or gerber, not {format}")}); }+    let inch = args["units"].as_str() == Some("in");+    let side = args["side"].as_str().unwrap_or("both");+    let rs = crate::routing::call("routing_state", args, pass);+    if rs["success"].as_bool() != Some(true) { return rs; }+    // EAGLE's populate="no" (the current variant's not-fitted parts) from the board source+    let src = crate::routing::call("board_source", args, pass);+    let dnp: Vec<String> = src["source"].as_str().or(src["data"]["source"].as_str()).map(|t| {+        regex::Regex::new(r#"<element name="([^"]+)"[^>]*populate="no""#).unwrap().captures_iter(t).map(|c| c[1].to_string()).collect()+    }).unwrap_or_default();+    let pads = rs["pads"].as_array().cloned().unwrap_or_default();+    let mut parts: Vec<Placed> = Vec::new();+    for c in rs["components"].as_array().into_iter().flatten() {+        let r = c["ref"].as_str().unwrap_or("").to_string();+        let mine: Vec<&Value> = pads.iter().filter(|p| p["owner"].as_str() == Some(r.as_str()) || p["ref"].as_str() == Some(r.as_str())).collect();+        if mine.is_empty() { continue; } // a part with no pads (a frame, a logo) is not placed by a machine+        // SMD when any pad is a surface pad: a power part's footprint may carry drilled thermal vias too+        let smd = mine.iter().any(|p| p["drill"].as_f64().unwrap_or(0.0) == 0.0);+        parts.push(Placed { refdes: r, value: c["value"].as_str().unwrap_or("").into(), footprint: c["pattern"].as_str().unwrap_or("").into(),+            x: c["x"].as_f64().unwrap_or(0.0), y: c["y"].as_f64().unwrap_or(0.0), rot: c["rotation"].as_f64().unwrap_or(0.0), top: c["side"] != "bottom", smd });+    }+    let exclude_dnp = args["excludeDnp"].as_bool() != Some(false);+    let smd_only = args["smdOnly"].as_bool() == Some(true);+    parts.retain(|p| !(exclude_dnp && dnp.contains(&p.refdes)) && !(smd_only && !p.smd));+    parts.sort_by(|a, b| a.refdes.cmp(&b.refdes));+    let dir = args["outputDir"].as_str().map(|s| s.replace('\\', "/")).unwrap_or_else(|| format!("{}/adom-pos", std::env::temp_dir().to_string_lossy().replace('\\', "/")));+    if let Err(e) = std::fs::create_dir_all(&dir) { return json!({"success": false, "errorCode": "write_failed", "error": e.to_string()}); }+    let stem = rs["documentName"].as_str().unwrap_or("board").chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect::<String>();+    let k = if inch { 1.0 / 25.4 } else { 1.0 };+    let unit = if inch { "in" } else { "mm" };+    let mut files = Vec::new();+    let mut counts = json!({});+    let sides: Vec<(&str, bool)> = match side { "front" | "top" => vec![("front", true)], "back" | "bottom" => vec![("back", false)], _ => vec![("front", true), ("back", false)] };+    for (name, top) in &sides {+        let rows: Vec<&Placed> = parts.iter().filter(|p| p.top == *top).collect();+        counts[*name] = json!(rows.len());+        let path = format!("{dir}/{stem}-pos-{name}.{}", if format == "csv" { "csv" } else { "txt" });+        let mut s = String::new();+        if format == "csv" {+            s.push_str(&format!("Ref,Val,Package,PosX({unit}),PosY({unit}),Rot,Side\n"));+            for p in &rows { s.push_str(&format!("\"{}\",\"{}\",\"{}\",{:.4},{:.4},{:.2},{}\n", p.refdes, p.value, p.footprint, p.x * k, p.y * k, p.rot, if p.top { "top" } else { "bottom" })); }+        } else {+            s.push_str(&format!("### Footprint positions, {unit}, {name}\n# Ref     Val              Package                PosX       PosY       Rot  Side\n"));+            for p in &rows { s.push_str(&format!("{:<9} {:<16} {:<22} {:>10.4} {:>10.4} {:>6.2}  {}\n", p.refdes, p.value, p.footprint, p.x * k, p.y * k, p.rot, if p.top { "top" } else { "bottom" })); }+            s.push_str("## End\n");+        }+        if let Err(e) = std::fs::write(&path, s) { return json!({"success": false, "errorCode": "write_failed", "error": e.to_string()}); }+        files.push(path);+    }+    if args["fab"].as_str().map(|f| f.eq_ignore_ascii_case("jlcpcb")).unwrap_or(false) {+        let path = format!("{dir}/{stem}-cpl-jlcpcb.csv");+        let mut s = String::from("Designator,Mid X,Mid Y,Layer,Rotation\n");+        for p in &parts { s.push_str(&format!("{},{:.4}mm,{:.4}mm,{},{:.0}\n", p.refdes, p.x, p.y, if p.top { "Top" } else { "Bottom" }, p.rot)); }+        if let Err(e) = std::fs::write(&path, s) { return json!({"success": false, "errorCode": "write_failed", "error": e.to_string()}); }+        files.push(path);+    }+    json!({"success": true, "mutated": false, "files": files, "parts": counts, "units": unit, "format": format, "dnpLeftOut": if exclude_dnp { json!(dnp) } else { json!([]) },+           "_hint": "Positions are each part's origin in the board frame, the way Fusion places it; JLCPCB checks rotations against its own library on upload."})+}++/// `fusion_window_info`: Fusion's top-level windows and its open dialogs, without screenshots.+pub fn window_info(ctx: &Ctx) -> Value {+    let Some(ab) = fusion_core::ab::Loopback::from_env() else { return json!({"success": false, "errorCode": "ab_unreachable", "error": "Adom Bridge did not answer on this machine"}) };+    let facts = fusion_core::windows_facts::facts_from_ab(&ab, &ctx.caller);+    let windows: Vec<Value> = facts.family.iter().map(|w| json!({"hwnd": w["hwnd"], "title": w["title"], "kind": w["kind"], "className": w["className"], "pid": w["pid"], "rect": w["rect"]})).collect();+    let main = facts.main_hwnd;+    let dialogs: Vec<Value> = crate::lifecycle::dialog_inventory(ctx).into_iter().map(|d| json!({"hwnd": d["hwnd"], "title": d["title"], "body": d["text"], "buttons": d["buttons"],+        "category": d["category"], "safeAction": d["safeButton"].as_str().or(d["resolution"].as_str()), "ownedByMainInstance": true})).collect();+    let modal = !dialogs.is_empty();+    json!({"success": true, "mutated": false, "windows": windows, "dialogs": dialogs, "hasModalDialogs": modal, "mainHwnd": main, "mainTitle": facts.main_title})+}++#[cfg(test)]+mod tests {+    use super::*;+    #[test]+    fn a_library_lists_its_components_or_its_footprints() {+        let p = std::env::temp_dir().join(format!("adom-ll-{}.lbr", std::process::id()));+        std::fs::write(&p, r#"<eagle><drawing><library><packages><package name="R0603"/><package name="SOT23"/></packages><symbols/><devicesets><deviceset name="R"/></devicesets></library></drawing></eagle>"#).unwrap();+        let path = p.to_string_lossy().to_string();+        let s = list_library(&json!({"libraryPath": path}));+        assert_eq!((s["kind"].as_str(), s["data"]["names"].clone()), (Some("sch"), json!(["R"])));+        let f = list_library(&json!({"libraryPath": path, "kind": "pcb", "limit": 1}));+        assert_eq!((f["data"]["count"].as_u64(), f["data"]["names"].clone()), (Some(2), json!(["R0603"])));+        let _ = std::fs::remove_file(&p);+    }+}
tests/run_verb_tests.py+6
@@ -440,6 +440,12 @@ t("fusion_tool_update", "daily", {"action": "check"}, {"ok": True, "fields": ["u t("fusion_export_3d#vrml", "negative", {"format": "vrml", "outputPath": f"{SCRATCH}/board.wrl"}, {"errorCode": "not_supported"}, "a format Fusion has no exporter for is refused by name") t("fusion_export_3d", "daily", dict(B, format="stl", outputPath=f"{SCRATCH}/board-3d.stl"), {"ok": True, "fields": ["path", "bytes"]}, "the design's 3D PCB as STL through the one export_3d verb") t("fusion_edit_pours", "manual", note="writes pours: tested live on lane-BQ25792-copy (thermals and rank set, read back, set back)")+t("fusion_board_summary", "daily", B, {"ok": True, "fields": ["summary"]}, "the standard's summary shaped from read_board")+t("fusion_read_schematic", "daily", B, {"ok": True, "fields": ["components", "designators"]}, "the standard's read_schematic from schematic_data")+t("fusion_list_library", "daily", {"libraryPath": "C:/Users/olive/Documents/fusion-lane-tests/plate/plate-test.lbr", "kind": "pcb"}, {"ok": True, "fields": ["data", "kind"]}, "offline .lbr names")+t("fusion_export_pos", "daily", dict(B, fab="jlcpcb", outputDir=f"{SCRATCH}/pos"), {"ok": True, "fields": ["files", "parts"]}, "pick-and-place per side plus JLCPCB's CPL")+t("fusion_export_pos#gerber", "negative", dict(B, format="gerber"), {"errorCode": "not_supported"}, "no Gerber position file in Fusion: refused by name")+t("fusion_window_info", "daily", {}, {"ok": True, "fields": ["windows", "dialogs", "hasModalDialogs"]}, "read-only: windows and dialogs, no screenshots") t("fusion_log_tail", "daily", {"sinceOffset": 0, "maxBytes": 20000, "errorsOnly": True}, {"ok": True, "fields": ["entries", "next", "truncated"]}, "read-only: Fusion's own log since an offset") t("fusion_verb_times", "daily", {"limit": 20}, {"ok": True, "fields": ["verbs", "since"]}) t("fusion_live_feed", "daily", {"action": "poll"}, {"ok": True}, "one poll of the feed; no waiting")