← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.52\ No newline at end of file+1.13.53\ No newline at end of file
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.53 (2026-11-02)++Partials on the EDA Bridge Standard, schematic edits first, and three candidates from standard #29:+- `fusion_sch_place_part` takes `partId` (a gate of a multi-gate device), and works from a local .lbr (it did since #135).+- `fusion_sch_swap_part`: where a new pin ends elsewhere, a wire joins it to where the old pin ended (`pinsJoined`,+  by pad number). `lostConnections` is what's left over.+- `fusion_edit_parts`: `dnp`, `excludeFromBom`, `excludeFromPos` and `mount` are attributes on the part, read back,+  and `fusion_board_bom` (`excluded`), `fusion_bom` and `fusion_export_pos` honour them. Fusion's own variants and CAM+  outputs do not read them.+- `fusion_update_pcb` (new): the standard's preview shape. Execute has nothing to run; removals are refused.+- `fusion_preferences` (new): read, or change with `set`. `fusion_select_items` (new): select by designator, or clear.+- `fusion_schematic_run` and `fusion_sch_place_part` refuse a position past 1950 mm (`off_the_sheet`): Fusion dropped such+  a part silently, said "no such device", and left its name unusable.+ ## 1.13.52 (2026-11-02)  Issue #136, the actual cause, reproduced with the reporter's library and bisected live on Fusion 2706.1.14: Fusion raises
bridge.json+4−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.52",+  "version": "1.13.53",   "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.",@@ -247,6 +247,7 @@     "fusion_plan_route",     "fusion_plate_vias",     "fusion_pour_state",+    "fusion_preferences",     "fusion_preflight_lbr",     "fusion_prefs_close",     "fusion_prefs_open",@@ -331,6 +332,7 @@     "fusion_schematic_run",     "fusion_screenshot_fusion",     "fusion_search_cloud_files",+    "fusion_select_items",     "fusion_select_net",     "fusion_set_auto_update",     "fusion_set_design_rules",@@ -378,6 +380,7 @@     "fusion_uninstall",     "fusion_update_board_from_eagle",     "fusion_update_footprints_from_library",+    "fusion_update_pcb",     "fusion_variants",     "fusion_verb_times",     "fusion_via_fence",
contracts/describe.json+62−3
@@ -1959,7 +1959,7 @@         "revision": "str"       },       "statusVerb": null,-      "summary": "Edit placed parts (the standard's edit_parts): value, designator (newRef), any field (attribute; null removes it) and the lock, each read back. Fusion keeps board and schematic in step, so the edit is in the schematic too. dnp, excludeFromBom, excludeFromPos and mount are refused per part (Fusion keeps them in assembly variants).",+      "summary": "Edit placed parts (the standard's edit_parts): value, designator (newRef), any field (attribute; null removes it) and the lock, each read back. Fusion keeps board and schematic in step, so the edit is in the schematic too. dnp, excludeFromBom, excludeFromPos and mount are attributes on the part (DNP, EXCLUDE_FROM_BOM, EXCLUDE_FROM_POS, MOUNT), which fusion_board_bom, fusion_bom and fusion_export_pos honour; Fusion's own assembly variants and CAM outputs do not read them.",       "timeoutSeconds": 150     },     {@@ -20511,6 +20511,25 @@       "summary": "Read every pour on the live board with Fusion's own computed fill: filled area per pour, per layer and per net, and which pours were never filled (no RATSNEST since they were drawn). The measure step of the AI flow's Fusion lane.",       "timeoutSeconds": 60     },+    {+      "example": {+        "set": {+          "lengthUnit": "mm"+        }+      },+      "input": {+        "set": "optional {key: value}: the keys fusion_set_preference takes"+      },+      "longRunning": false,+      "name": "fusion_preferences",+      "output": {+        "applied": "obj (with set)",+        "current": "obj"+      },+      "statusVerb": "fusion_get_app_state",+      "summary": "Read Fusion's own settings (theme, scroll direction, orbit scheme, orientation, length unit), or change them with set, live with no restart. The standard's preferences: fusion_get_preferences and fusion_set_preference as one verb.",+      "timeoutSeconds": 30+    },     {       "example": {         "libraryPath": "C:/Temp/PART.lbr"@@ -22157,7 +22176,7 @@         "saved": "false"       },       "statusVerb": null,-      "summary": "Place a part on the schematic (the standard's sch_place_part): a deviceset from a .lbr on the Fusion machine at x, y (mil) and a rotation, with its designator (R? or none: the next free number) and value (comment). footprint picks the variant whose package has that name. The device is proven in the .lbr and the library USEd first, and ADD's questions are answered, so no picker or dialog opens; read back as the one part added, or undone. Fusion adds it to the board too.",+      "summary": "Place a part on the schematic (the standard's sch_place_part): a deviceset from a .lbr on the Fusion machine at x, y (mil) and a rotation, with its designator (R? or none: the next free number) and value (comment). footprint picks the variant whose package has that name. partId picks the gate of a multi-gate device. The device is proven in the .lbr and the library USEd first, and ADD's questions are answered, so no picker or dialog opens; a position past 1950 mm is refused (off_the_sheet); read back as the one part added, or undone. Fusion adds it to the board too.",       "timeoutSeconds": 120     },     {@@ -22264,7 +22283,7 @@         "pinsJoined": "[] (Fusion does not add wires)"       },       "statusVerb": null,-      "summary": "Replace a placed part's device (the standard's sch_swap_part) with EAGLE's REPLACE: same name, place and turn, a deviceset from a .lbr on the Fusion machine (footprint picks the variant). Fusion keeps the connections of pins that land where the old ones were; the reply compares every pin's net by pad number and lists lostConnections to wire with fusion_sch_connect. keepComment puts the old value back. Multi-gate devices are refused. Read back, or undone.",+      "summary": "Replace a placed part's device (the standard's sch_swap_part) with EAGLE's REPLACE: same name, place and turn, a deviceset from a .lbr on the Fusion machine (footprint picks the variant). Fusion keeps the connections of pins that land where the old ones were; where a new pin ends elsewhere a wire joins it to where the old pin ended (pinsJoined, matched by pad number), and lostConnections names any pin left without its net. keepComment puts the old value back. Multi-gate devices are refused. Read back, or undone.",       "timeoutSeconds": 120     },     {@@ -22540,6 +22559,28 @@       "summary": "\u26d4 DISABLED (returns an error): the in-app add-in search took 30+ min and CRASHED Fusion (WinError 10054). It is hard-blocked in the bridge. Use fusion_aps_search {query} \u2014 server-indexed, seconds, no Fusion. See the 'fusion-aps-search' skill.",       "timeoutSeconds": 15     },+    {+      "example": {+        "refs": [+          "R1",+          "R2"+        ]+      },+      "input": {+        "clear": "bool",+        "refs": "[designator]"+      },+      "longRunning": false,+      "name": "fusion_select_items",+      "output": {+        "failed": "[{ref, error}]",+        "selected": "[ref]",+        "verified": "false"+      },+      "statusVerb": "fusion_get_app_state",+      "summary": "Select parts in the open schematic or board by designator, or clear the selection (the standard's select_items). Fusion does not report its electronics selection back, so the reply cannot verify it (verified:false): look at the view.",+      "timeoutSeconds": 30+    },     {       "example": {         "net": "VBUS",@@ -23619,6 +23660,24 @@       "summary": "Compare placed footprints with their library (the standard's update_footprints_from_library), the plan: each part's footprint as the board holds it against the same-named package in the .lbr files named, pad by pad (added, removed, moved, resized, reshaped, drill, turn, layer), each up_to_date, differs or library_not_found. A Fusion design's libraries are cloud items, so the .lbr files are named. apply:true is refused (not_supported) with the plan: Fusion's UPDATE asks about each part in a dialog.",       "timeoutSeconds": 120     },+    {+      "example": {},+      "input": {+        "allowRemovals": "bool, default false",+        "execute": "bool, default false",+        "expectDocument": "the design (default the one in front)"+      },+      "longRunning": false,+      "name": "fusion_update_pcb",+      "output": {+        "changeOrder": "[] (nothing to run)",+        "executed": "bool",+        "preview": "{addParts, removeParts, sheetParts, boardParts}"+      },+      "statusVerb": "fusion_get_app_state",+      "summary": "The standard's update_pcb. Fusion forward-annotates the board from the schematic as you edit, so there is no change order to run: the preview compares the sheet and the board (addParts on the sheet with no package, removeParts only on the board), and execute has nothing to carry over. allowRemovals with parts to remove is refused: Fusion cannot delete a part from a linked board by command.",+      "timeoutSeconds": 60+    },     {       "example": {},       "input": {
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.13.52"+version = "1.13.53" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.52"+version = "1.13.53" edition = "2021"  [dependencies]
describe.py+10−3
@@ -246,7 +246,7 @@ _T = [     ("fusion_bom", "The bill of materials (the standard's bom) through the shared engine: the schematic's parts with their attributes, the live board and the variant (variant names one; default the current), grouped one line per manufacturer part number, checked for what an order needs (missing MPN, maker, Mouser/DigiKey/LCSC numbers, lines mixing values, JLCPCB lines without LCSC, DNP, sheet and board mismatches), and the order files written to outputDir: bom.csv, bom-jlcpcb.csv, cpl-jlcpcb.csv, bom-mouser.csv, bom-digikey.csv.",      {"expectDocument": "the board's name", "variant": "a variant's name (default the current one)", "boards": "int, default 1: multiplies quantities", "outputDir": "folder for bom.csv and bom-jlcpcb.csv", "write": "bool, default true"},      {"lines": "[{quantity, designators, value, comment, description, footprint, package, mpn, manufacturer, mouser, digikey, lcsc, libReference}]", "fittedParts": "int", "checks": "{missingMpn, missingManufacturer, missingPartNumbers, inconsistent, dnp, excludedKinds, sheetNotOnBoard, boardNotOnSheet}", "variants": "[str]", "alternates": "[{designator, alternatePart}]", "files": "[path] (write:false answers the files' text instead)", "outputDir": "str"}, 180, None, False, {}),-    ("fusion_edit_parts", "Edit placed parts (the standard's edit_parts): value, designator (newRef), any field (attribute; null removes it) and the lock, each read back. Fusion keeps board and schematic in step, so the edit is in the schematic too. dnp, excludeFromBom, excludeFromPos and mount are refused per part (Fusion keeps them in assembly variants).",+    ("fusion_edit_parts", "Edit placed parts (the standard's edit_parts): value, designator (newRef), any field (attribute; null removes it) and the lock, each read back. Fusion keeps board and schematic in step, so the edit is in the schematic too. dnp, excludeFromBom, excludeFromPos and mount are attributes on the part (DNP, EXCLUDE_FROM_BOM, EXCLUDE_FROM_POS, MOUNT), which fusion_board_bom, fusion_bom and fusion_export_pos honour; Fusion's own assembly variants and CAM outputs do not read them.",      {"edits": "[{ref, value?, newRef?, fields?: {name: value | null}, locked?: bool}] (required)", "expectDocument": "required: the board's name", "expectedRevision": "optional", "dryRun": "bool"},      {"edited": "[{ref, after: {ref, value, fields, locked}}]", "failed": "[{ref, error}]", "revision": "str"},      150, None, False, {"edits": [{"ref": "R5", "value": "10k", "fields": {"MPN": "RC0402FR-0710KL"}}]}),@@ -594,7 +594,7 @@ _T = [      {"expectDocument": "the design", "analysis": "{type: op} | {type: dc, source, start, stop, step} | {type: ac, sweep: dec|oct|lin, points, start, stop} | {type: tran, step, stop, start?} | {type: noise, source, output, reference?, sweep, points, start, stop} | {type: raw, text}", "probes": "[{net} | {current: source}]", "points": "per signal, default 200 (a ceiling: first, last and extrema kept)", "maxProbes": "default 32", "sweep": "{parameter: R4 | Q1.BF, start, stop, step, scale?: lin|dec|oct}", "temperatures": "{start, stop, step} degC", "monteCarlo": "{runs, distribution: uniform|gaussian, seed, tolerances: {resistor, capacitor, inductor} %}: every R/C/L with a plain value drawn, a signal per run with the drawn values as step", "maxRuns": "default 50", "netlistPath": "a .net/.cir on the Fusion machine instead of the design", "timeoutSec": "default 120"},      {"signals": "[{signal, x, y, xUnit, yUnit, phase?, step?}]", "missing": "[probe]", "notSimulated": "[ref]", "totals": "noise: {onoise, inoise} V (a list with step when there are several runs)", "available": "[net]", "engine": "str", "analysis": "str", "rows": "int", "runs": "int", "netlist": "path"},      300, None, False, {'analysis': {'type': 'ac', 'sweep': 'dec', 'points': 20, 'start': 10, 'stop': 1000000}, 'probes': [{'net': 'OUT'}]}),-    ('fusion_sch_place_part', "Place a part on the schematic (the standard's sch_place_part): a deviceset from a .lbr on the Fusion machine at x, y (mil) and a rotation, with its designator (R? or none: the next free number) and value (comment). footprint picks the variant whose package has that name. The device is proven in the .lbr and the library USEd first, and ADD's questions are answered, so no picker or dialog opens; read back as the one part added, or undone. Fusion adds it to the board too.",+    ('fusion_sch_place_part', "Place a part on the schematic (the standard's sch_place_part): a deviceset from a .lbr on the Fusion machine at x, y (mil) and a rotation, with its designator (R? or none: the next free number) and value (comment). footprint picks the variant whose package has that name. partId picks the gate of a multi-gate device. The device is proven in the .lbr and the library USEd first, and ADD's questions are answered, so no picker or dialog opens; a position past 1950 mm is refused (off_the_sheet); read back as the one part added, or undone. Fusion adds it to the board too.",      {"expectDocument": "the design", "library": ".lbr path on the Fusion machine (required)", "symbol": "the deviceset (required)", "designator": "e.g. R12 or R? (default the deviceset's prefix, next free)", "comment": "the value", "footprint": "a package name of the deviceset's variants", "x": "mil, default 1000", "y": "mil, default 1000", "rotation": "0|90|180|270", "sheet": "int, default the one shown", "dryRun": "bool"},      {"designator": "str", "libref": "dev@lib", "atMil": "[x, y]", "pins": "int", "footprints": "[package]", "saved": "false"},      120, None, False, {'library': 'C:/parts/Lib.lbr', 'symbol': 'R-US', 'designator': 'R?', 'comment': '10k', 'x': 3000, 'y': 4000}),@@ -602,7 +602,7 @@ _T = [      {"expectDocument": "the design", "connections": "[{designator, pin (pad number or name), net, kind?: label|power|port|wire, stubMil?}] (required)", "sheet": "int, default the one shown", "dryRun": "bool"},      {"connected": "int", "results": "[{designator, pin, net, kind, result, endMil}]", "failed": "[{designator, pin, error, code}]", "saved": "false"},      120, None, False, {'connections': [{'designator': 'U2', 'pin': '1', 'net': 'GND', 'kind': 'power'}, {'designator': 'U2', 'pin': 'VIN', 'net': 'VIN'}]}),-    ('fusion_sch_swap_part', "Replace a placed part's device (the standard's sch_swap_part) with EAGLE's REPLACE: same name, place and turn, a deviceset from a .lbr on the Fusion machine (footprint picks the variant). Fusion keeps the connections of pins that land where the old ones were; the reply compares every pin's net by pad number and lists lostConnections to wire with fusion_sch_connect. keepComment puts the old value back. Multi-gate devices are refused. Read back, or undone.",+    ('fusion_sch_swap_part', "Replace a placed part's device (the standard's sch_swap_part) with EAGLE's REPLACE: same name, place and turn, a deviceset from a .lbr on the Fusion machine (footprint picks the variant). Fusion keeps the connections of pins that land where the old ones were; where a new pin ends elsewhere a wire joins it to where the old pin ended (pinsJoined, matched by pad number), and lostConnections names any pin left without its net. keepComment puts the old value back. Multi-gate devices are refused. Read back, or undone.",      {"expectDocument": "the design", "designator": "required", "library": ".lbr path (required)", "symbol": "the new deviceset (required)", "footprint": "package of the variant", "keepComment": "bool, default false", "dryRun": "bool"},      {"now": "{libReference, footprints}", "oldPinsNotOnNew": "[pad]", "newPinsNotOnOld": "[pad]", "lostConnections": "[{pin, net}]", "pinsJoined": "[] (Fusion does not add wires)"},      120, None, False, {'designator': 'C10', 'library': 'C:/parts/Lib.lbr', 'symbol': 'C', 'footprint': 'CAPC1005X60'}),@@ -665,6 +665,13 @@ _T = [     ("fusion_schematic_run", "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. A .lbr Fusion 2706 cannot ADD from (no line breaks between elements, a byte-order mark, single-quoted attributes, a 3D package naming a footprint the file does not have) is rewritten in place first, keeping its element tree apart from those dead 3D entries (libraryReformatted, library3dEntriesRemoved). The bridge sends USE itself, for the file named in libraryLoaded. Readback: the parts and nets on the sheet from a fresh export.",      {"expectDocument":"optional exact schematic name","libraryPath":"required for ADD: the .lbr on this machine (its file name is the library name in ADD)","commands":"required [command] (1..400)","pauseMs":"optional 0..3000 between commands"},      {"success":"bool","verified":"bool","parts":"[name]","added":"[name]","nets":"{net:[part.pin]}","commands":"int","libraryLoaded":"path","libraryReformatted":"bool","library3dEntriesRemoved":"[name]"},120,"fusion_get_app_state",False,{}),+    ("fusion_update_pcb", "The standard's update_pcb. Fusion forward-annotates the board from the schematic as you edit, so there is no change order to run: the preview compares the sheet and the board (addParts on the sheet with no package, removeParts only on the board), and execute has nothing to carry over. allowRemovals with parts to remove is refused: Fusion cannot delete a part from a linked board by command.",+     {"expectDocument":"the design (default the one in front)","execute":"bool, default false","allowRemovals":"bool, default false"},+     {"executed":"bool","changeOrder":"[] (nothing to run)","preview":"{addParts, removeParts, sheetParts, boardParts}"},60,"fusion_get_app_state",False,{}),+    ("fusion_preferences", "Read Fusion's own settings (theme, scroll direction, orbit scheme, orientation, length unit), or change them with set, live with no restart. The standard's preferences: fusion_get_preferences and fusion_set_preference as one verb.",+     {"set":"optional {key: value}: the keys fusion_set_preference takes"},{"current":"obj","applied":"obj (with set)"},30,"fusion_get_app_state",False,{"set":{"lengthUnit":"mm"}}),+    ("fusion_select_items", "Select parts in the open schematic or board by designator, or clear the selection (the standard's select_items). Fusion does not report its electronics selection back, so the reply cannot verify it (verified:false): look at the view.",+     {"refs":"[designator]","clear":"bool"},{"selected":"[ref]","failed":"[{ref, error}]","verified":"false"},30,"fusion_get_app_state",False,{"refs":["R1","R2"]}),     ("fusion_board_bom", "The bill of materials from the live board's own part attributes: one line per MPN with designators, quantity, value, package, manufacturer and the Mouser, DigiKey and LCSC numbers, plus the JLCPCB form (Comment, Designator, Footprint, LCSC). Board-only parts (test pads) are listed as such, and any part missing a distributor number is named. Read-only; with outputDir it also writes bom.csv and bom-jlcpcb.csv there.",      {"expectDocument":"required exact board name","outputDir":"optional folder on this machine for bom.csv + bom-jlcpcb.csv"},      {"lines":"[{designators,quantity,value,package,mpn,manufacturer,mouser,digikey,lcsc}]","csv":"str","jlcpcbCsv":"str","files":"{name:path}","boardOnly":"[str]","missingPartNumbers":"{mouser,digikey,lcsc:[str]}"},60,"fusion_routing_state",False,{}),
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.52"+DASH_VERSION="1.13.53" 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.52",+  "version": "1.13.53",   "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.52",+  "version": "1.13.53",   "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.52"+version = "1.13.53" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.52"+version = "1.13.53" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.52"+version = "1.13.53" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.52"+version = "1.13.53" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.52"+version = "1.13.53" 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.52"+version = "1.13.53" edition = "2021" license = "MIT" 
rust/bridge/src/main.rs+1−1
@@ -567,7 +567,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_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), "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_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 },                     "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/routing_script.py+32−6
@@ -640,6 +640,9 @@ def sch_snapshot(app):     return doc.name, parts, {k: sorted(v) for k, v in nets.items()}  +SHEET_LIMIT_MM = 1950.0++ def sch_run(app, args):     """Run a list of schematic commands. ADD is proven against the .lbr on disk first (the     device must exist there), so the component picker can never open."""@@ -663,6 +666,13 @@ def sch_run(app, args):             if not lbr: fail('library_required', 'ADD needs libraryPath so the device is proven to exist before ADD runs')             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))+    # Fusion silently drops a part placed about 2 m out (a small part landed at 1988 mm, a larger one failed+    # at 1990 mm and 2100 mm: live on 2706), answers nothing, and leaves that part name unusable afterwards+    for c in cmds:+        if c.strip().startswith('ADD '):+            at = re.search(r'\(\s*(-?[0-9.]+)mm\s+(-?[0-9.]+)mm\s*\)', c)+            if at and max(abs(float(at.group(1))), abs(float(at.group(2)))) > SHEET_LIMIT_MM:+                fail('off_the_sheet', 'Fusion cannot place a part this far out (past about 2 m it drops it silently and the name stays unusable): %s. Keep positions within %g mm; nothing was run.' % (c.strip(), SHEET_LIMIT_MM))     done = []     # ADD takes the device from the .lbr loaded with USE. (Fusion cannot take a library out of use from a     # command, so two in-use libraries with the same name at different paths make ADD ask which one.)@@ -727,7 +737,11 @@ def board_bom(board, args):     as board-only, never dropped silently."""     import csv, io     rows = {}+    excluded = []     for e in board.iter('element'):+        flagged = {a.get('name') for a in e.findall('attribute') if str(a.get('value', '')).lower() in ('yes', 'true', '1')}+        if e.get('populate') == 'no' or flagged & {'DNP', 'EXCLUDE_FROM_BOM'}:+            excluded.append(e.get('name')); continue         ids = part_ids(e)        # MPN, LCSC... under any of the names libraries use (board_model.py)         mpn = ids['mpn']         # without an MPN, parts are one line only when footprint, value and LCSC agree: grouping by@@ -758,7 +772,7 @@ def board_bom(board, args):         for name, text in (('bom.csv', out.getvalue()), ('bom-jlcpcb.csv', jo.getvalue())):             (Path(d) / name).write_text(text, encoding='utf-8'); files[name] = (Path(d) / name).as_posix()     missing = {k: [' '.join(r['designators']) for r in lines if r['mpn'] and not r[k]] for k in ('mouser', 'digikey', 'lcsc')}-    return {'lines': lines, 'csv': out.getvalue(), 'jlcpcbCsv': jo.getvalue(), 'files': files,+    return {'lines': lines, 'excluded': sorted(excluded), 'csv': out.getvalue(), 'jlcpcbCsv': jo.getvalue(), 'files': files,             'boardOnly': [' '.join(r['designators']) for r in lines if not r['mpn']], 'missingPartNumbers': missing}  @@ -2061,8 +2075,9 @@ def _guard(meta, args):   def edit_parts(app, root, board, meta, args):-    """Value, designator, fields and the lock of placed parts; each edit read back. Fusion keeps-    board and schematic in step, so a value or name changed here is changed in the schematic too."""+    """Value, designator, fields, the lock, do-not-populate, BOM and position exclusion and mounting type+    of placed parts; each edit read back. Fusion keeps board and schematic in step, so a value or name+    changed here is changed in the schematic too."""     _guard(meta, args)     edits = args.get('edits') or []     if not edits:@@ -2075,9 +2090,20 @@ def edit_parts(app, root, board, meta, args):         if el is None:             failed.append({'ref': ref, 'error': 'no part %s on the board' % ref})             continue-        refused = [k for k in ('dnp', 'excludeFromBom', 'excludeFromPos', 'mount') if k in ed]-        if refused:-            failed.append({'ref': ref, 'error': 'unsupported: %s (Fusion keeps do-not-populate and BOM exclusion in assembly variants, which this verb does not edit)' % ', '.join(refused)})+        # do-not-populate, BOM and position exclusion and mounting type: attributes on the part, which+        # fusion_board_bom, fusion_bom and fusion_export_pos honour. Fusion's assembly variants cannot be+        # changed from a command (VARIANT does nothing on 2706), so its own CAM outputs do not read them.+        have = {a.get('name') for a in el.findall('attribute')}+        flags = dict(ed.get('fields') or {})+        for key, attr in (('dnp', 'DNP'), ('excludeFromBom', 'EXCLUDE_FROM_BOM'), ('excludeFromPos', 'EXCLUDE_FROM_POS')):+            if key in ed:+                if ed[key]: flags[attr] = 'yes'+                elif attr in have: flags[attr] = None+        if 'mount' in ed:+            if ed['mount'] not in ('smd', 'through_hole', 'other'):+                failed.append({'ref': ref, 'error': 'mount is smd, through_hole or other'})+            else: flags['MOUNT'] = ed['mount']+        if flags: ed = dict(ed, fields=flags)         cmds, cur = [], ref         if ed.get('value') is not None:             cmds.append("SET CONFIRM YES; VALUE '%s' %s; SET CONFIRM OFF;" % (identifier(cur), "'" + str(ed['value']).replace("'", "''") + "'"))
rust/bridge/src/sch_script.py+27−3
@@ -1081,7 +1081,15 @@ def sch_place_part(app, args):     if rot not in (0, 90, 180, 270):         sfail('bad_args', 'rotation is 0, 90, 180 or 270')     x, y = float(args.get('x', 1000)) * 0.0254, float(args.get('y', 1000)) * 0.0254-    e.cmds.append("SET CONFIRM YES; ADD '%s@%s' '%s' R%d %s; SET CONFIRM OFF;" % (full, libname, ref, rot, mm((x, y))))+    # partId: which gate of a multi-gate device (1 the first). Fusion adds one gate of a part per ADD, named+    gates_ = ds.findall('gates/gate')+    part_id = int(args.get('partId') or 1)+    if not 1 <= part_id <= max(len(gates_), 1):+        sfail('bad_args', 'partId %d: %s has %d gate(s)' % (part_id, sym, len(gates_)))+    gate_arg = " '%s'" % gates_[part_id - 1].get('name') if part_id > 1 else ''+    if max(abs(x), abs(y)) > 1950:+        sfail('off_the_sheet', 'Fusion cannot place a part this far out (past about 2 m it drops it silently and the name stays unusable): keep x and y within 76770 mil')+    e.cmds.append("SET CONFIRM YES; ADD '%s@%s' '%s'%s R%d %s; SET CONFIRM OFF;" % (full, libname, ref, gate_arg, rot, mm((x, y))))     if args.get('comment'):         e.cmds.append('SET CONFIRM YES; VALUE %s %s; SET CONFIRM OFF;' % (quote(ref), quote(str(args['comment'])[:40])))     pins = sum(len(lroot.findall(".//symbols/symbol[@name='%s']/pin" % g.get('symbol'))) for g in ds.findall('gates/gate'))@@ -1133,7 +1141,14 @@ def sch_swap_part(app, args):             for pad in (q['number'] or q['pin']).split():                 out[pad] = q['net']         return out+    def ends(root, sheet):+        out = {}+        for q in pins_with_connections(root, sheet, [ref], connections=False):+            for pad in (q['number'] or q['pin']).split():+                out[pad] = tuple(q['at'])+        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])))@@ -1150,10 +1165,19 @@ def sch_swap_part(app, args):     if now_part is None or now_part.get('deviceset', '').upper() != ds.get('name').upper() or not e.same_except(parts=False, wires=False, junctions=False, labels=False):         e.undo('%s did not read back as %s' % (ref, full))     lost = sorted(p_ for p_, n in before.items() if n and after.get(p_) != n)+    # 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)     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=[],-                   _hint='Pins listed in lostConnections lost their net: wire them with fusion_sch_connect. Fusion updates the board footprint with the part.')+                   lostConnections=[{'pin': p_, 'net': before[p_]} for p_ in lost], pinsJoined=joined,+                   _hint='Fusion updates the board footprint with the part. A pin left in lostConnections has no pin of that pad number on the new part, or could not be joined: wire it with fusion_sch_connect.')   def sch_split_wire(app, args):
rust/bridge/src/stdshapes.rs+60−1
@@ -91,8 +91,13 @@ pub fn export_pos(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {     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);+    // and the parts marked on the board: edit_parts' DNP and EXCLUDE_FROM_POS attributes     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()+        let mut v: Vec<String> = regex::Regex::new(r#"<element name="([^"]+)"[^>]*populate="no""#).unwrap().captures_iter(t).map(|c| c[1].to_string()).collect();+        let block = regex::Regex::new(r#"(?s)<element name="([^"]+)"[^>]*[^/]>(.*?)</element>"#).unwrap();+        let flag = regex::Regex::new(r#"<attribute name="(?:DNP|EXCLUDE_FROM_POS)" value="(?i:yes|true|1)""#).unwrap();+        v.extend(block.captures_iter(t).filter(|c| flag.is_match(&c[2])).map(|c| c[1].to_string()));+        v     }).unwrap_or_default();     let pads = rs["pads"].as_array().cloned().unwrap_or_default();     let mut parts: Vec<Placed> = Vec::new();@@ -232,6 +237,16 @@ 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();+    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();+        nf.extend(marked);+        eargs["variant"]["notFitted"] = json!(nf);+    }     let bundle = json!({"sheetParts": crate::checks::sheet_parts(&sd), "board": inputs["board"], "args": eargs});     let mut r = adom_eda_checks::run("bom", &bundle);     if r["success"].as_bool() != Some(true) { return r; }@@ -402,3 +417,47 @@ mod tests {         let _ = std::fs::remove_file(&p);     } }++/// `fusion_update_pcb` (the standard's update_pcb): Fusion forward-annotates as you edit, so there is no+/// change order to run. The preview compares the sheet and the board; execute has nothing to carry over,+/// and it refuses removals: a part only on the board cannot be deleted from a command while the board is+/// linked to its schematic (DELETE does nothing there, live on 2706).+pub fn update_pcb(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    let cc = crate::sch::call("cross_check", args, pass);+    if cc["success"].as_bool() != Some(true) { return cc; }+    let (add, remove) = (cc["onSheetNotOnBoard"].clone(), cc["onBoardNotOnSheet"].clone());+    let execute = args["execute"].as_bool() == Some(true);+    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": [],+        "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!({}))+}++/// `fusion_select_items` (the standard's select_items): select parts by designator, or clear the selection.+/// Fusion does not report its electronics selection back (see selection), so nothing can read it back.+pub fn select_items(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    if args["clear"].as_bool() == Some(true) {+        let r = pass("run_modeling_script", json!({"script": "app.userInterface.activeSelections.clear()\nresult={'cleared':True}"}));+        return json!({"success": r["success"].as_bool() != Some(false), "cleared": true, "verified": false, "error": r["error"]});+    }+    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"}); }+    let (mut selected, mut failed) = (Vec::new(), Vec::new());+    for r in &refs {+        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)); }+    }+    json!({"success": failed.is_empty(), "selected": selected, "failed": failed, "verified": false,+        "_hint": "Fusion does not report its electronics selection back, so this cannot be read back: look at the view (fusion_take_screenshot)."})+}
tests/run_verb_tests.py+3
@@ -292,6 +292,9 @@ t("fusion_project_info", "daily", dict(B), {"ok": True, "fields": ["files", "boa t("fusion_schematic_data", "daily", dict(B, designator="C11", params=True), {"ok": True, "fields": ["components", "count"]}) t("fusion_part_parameters", "daily", dict(B), {"ok": True, "fields": ["names"]}) t("fusion_cross_check", "daily", dict(B), {"ok": True, "fields": ["matched", "onSheetNotOnBoard", "onBoardNotOnSheet"]})+t("fusion_update_pcb", "daily", dict(B), {"ok": True, "fields": ["executed", "preview", "changeOrder"]}, "the standard's update_pcb: the preview, nothing to execute")+t("fusion_preferences", "daily", {}, {"ok": True}, "the standard's preferences, read only here")+t("fusion_select_items", "negative", {}, {"errorCode": "bad_args"}, "refs or clear is required") t("fusion_schematic_review", "daily", dict(B), {"ok": True, "fields": ["engine"]}) t("fusion_pin_pad_check", "daily", dict(B), {"ok": True, "fields": ["engine"]}) t("fusion_doc_params", "daily", dict(B), {"ok": True, "fields": ["params"]})