app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
EDA Bridge Standard parity: set_track_width, remove_items, set_outline, board_shape, board_outline, set_layer_visibility, select_net, run_erc, autoroute; 10 standard aliases; export_optimized_glb compress option (issue #113)
12 files changed
+2348−11
CHANGELOG.md+28README.md+22−2bridge.json+20contracts/describe.json+382describe.py+51−1rust/bridge/src/main.rs+4rust/bridge/src/routing.rs+2−2rust/bridge/src/routing_script.py+397rust/bridge/src/services.rs+8−4tests/reports/verb-test-2026-10-27_1130.json+1334tests/run_verb_tests.py+14−2tests/test_routing.py+86CHANGELOG.md+28@@ -19,6 +19,34 @@ The EDA Bridge Standard 0.1.1 settlements (standard issue #2). layer after them); if copper, parts or the outline move, every command is undone. Verified on BQ25792: IC1 and R2 names moved, sized and turned exactly, C1's value hidden and shown, revision unchanged, display as before. +Standard parity: 20 more verbs (222 in the catalog; Fusion covers 77 of the standard's 182, was 56).++- `fusion_set_track_width {nets | netClass, layer?, widthMm}`: each track is picked with only its layer displayed,+ at the point along it farthest from other copper (a midpoint pick took a neighbour), in up to three read-back+ passes; only the asked tracks may change, and only in width, or every change is undone. A stub lying wholly+ under a wider track of its own net, or inside its own pad, is reported as `covered`.+- `fusion_remove_items {tracks:[{start,end,layer}], vias:[{position}]}`, given as `fusion_routing_state` returns+ them; read back as the board minus exactly those, or undone. `results[]` per item.+- `fusion_set_outline {outline:{rect:[x0,y0,w,h], cornerRadius?} | {points}}` replaces the Dimension-layer outline+ (rounded corners are arcs: Fusion's WIRE takes the angle bare, `+90`; `$90` and `@r` are ignored without an+ error). The outline is read back closed with the asked edges, or every command is undone. `fusion_board_shape`+ is its rectangle form; `fusion_board_outline` reports the edge (Fusion's Gerbers export layer 20 as the outline).+- `fusion_set_layer_visibility {layerId, displayed}`, read back from the board's layer flags.+- `fusion_select_net {net, zoom?}` highlights a whole net (Fusion clears a finished SHOW's highlight at once, so+ SHOW is left waiting for a click: a command-line prompt, not a dialog) and frames it.+- `fusion_run_erc`: Fusion's ERC on the board's schematic (its tab brought forward, the board's put back), every+ message read from `UL_SCHEMATIC.errors` with class, code, sheet and position.+- `fusion_autoroute {nets?}`: Fusion's autorouter on the given nets, or every net with unconnected copper; existing+ copper is kept (read back, or undone); DRC before and after. Refused before anything runs when a copper layer in+ use is off in the board's autorouter setup: Fusion would ask "used but not enabled, run anyway?", which neither+ SET CONFIRM nor AUTO LOAD answers. Verified on the routing fixture: 6 unconnected to 0, DRC 0 errors.+- Standard names answer through the bridge's own verbs (marked `aliasOf`): status, save, run_script, run_command,+ list_commands, dialog_log, signin_status, license_status, open_document, show_3d_chip. `fusion_check_dialogs`+ is now in the catalog.+- `fusion_export_optimized_glb {compress: draco | quantize | none}` passes the STEP service's compression override+ (page issue #113): `none` gives a plain GLB that wiki README and widget frames can load, since they cannot reach a+ Draco decoder. Omitted, a molecule is Draco-compressed as before.+ ## 1.12.0 (2026-10-27, insiders) The board model, to the [EDA Bridge Standard](https://wiki.adom.inc/adom/eda-bridge-standard) (fusion-lane thread).
README.md+22−2@@ -527,7 +527,7 @@ The AI does the whole setup - the user never gets handed a download link: ## Full verb catalog All verbs return a rich `_hint`. `fusion_describe '{}'` is the machine-readable source of truth; this table is generated from it.-_202 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`.+_222 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. **Lifecycle and readiness** @@ -558,6 +558,7 @@ _202 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. - `fusion_set_seat_takeover` - Read/toggle the mid-session Autodesk seat-takeover sweep (STICKY per user, default ON). - `fusion_signin` - SELF-DRIVING as of 1.9.32: launches Fusion if needed, clicks Sign In, opens the OAuth in the right profile, TYPES THE EMAIL and advances to the exact remaining... - `fusion_signin_2fa` - Submit Autodesk's emailed 6-DIGIT verification code to finish a Fusion sign-in, typed IN THE BACKGROUND (UIA SetValue + Invoke - never steals the user's...+- `fusion_signin_status` - The EDA Bridge Standard's name for fusion_account: the same verb, same arguments and reply (marked aliasOf). - `fusion_web_signin` - Sign a pup browser window into an Autodesk WEB surface (library.io, the hub portal at <hub>.autodesk360.com, Autodesk Account) via Google SSO. **Cloud files and APS**@@ -614,9 +615,12 @@ _202 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. **Schematic, board and routing** +- `fusion_autoroute` - Fusion's autorouter on the given nets, or on every net with unconnected copper. - `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... - `fusion_board_info` - Raw board geometry XML (copper/arcs/lines) for the open board.+- `fusion_board_outline` - Report the board edge the fab gets. Fusion's edge is its Dimension layer (20) and its Gerbers export it as the outline, so there is nothing to draw: this says... - `fusion_board_seed` - Write the design a new board starts from, as EAGLE source on this machine: the board outline, the fab's design rules (Fusion's own rules block with your...+- `fusion_board_shape` - A rectangular board outline from its corner and size: fusion_set_outline {outline:{rect:[x0Mm,y0Mm,wMm,hMm]}}. - `fusion_board_source` - Read the live board as EAGLE XML from a fresh export: parts, copper, vias, pours with Fusion's own computed fill, and the outline. - `fusion_board_summary` - The EDA Bridge Standard's name for fusion_board_info: the same verb, same arguments and reply (marked aliasOf). - `fusion_eagle_import_status` - The run record of a background fusion_new_electronics_from_eagle import: stage (started | done | failed), documentName, partsExpected (elements in the .brd)...@@ -682,6 +686,7 @@ _202 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. - `fusion_document_info` - Metadata for the ACTIVE document: name, product type, save state, version, parent project. - `fusion_interactive` - Is Fusion taking input RIGHT NOW? Reads the add-in's busy flag (a long bridge command in flight), Fusion's own hasActiveJobs, and the main thread's latency on... - `fusion_live_feed` - A ~1 fps look INTO Fusion while the bridge drives it, kept as a BOX-SIDE ring buffer (~10 min).+- `fusion_open_document` - The EDA Bridge Standard's name for fusion_open_design: the same verb, same arguments and reply (marked aliasOf). - `fusion_save_document` - Save the open electronics design IN PLACE as a new version (no Save As, no fork): resolves the design behind whichever tab is in front (or documentName), saves... - `fusion_screenshot_fusion` - Background PrintWindow capture of Fusion's MAIN window (largest same-title window; works occluded/bottom-of-z-order, never raises anything) or a specific... - `fusion_set_driving_badge` - Read/toggle the 'Adom is driving Fusion' TASKBAR OVERLAY BADGE (STICKY per user, default ON).@@ -695,10 +700,12 @@ _202 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. - `fusion_action_ledger` - Read the append-only record of what the bridge did in the BACKGROUND on this machine - dialogs it answered, Untitled scratch docs it swept, Fusion auto-starts... - `fusion_alerts` - Fusion's OWN alert toasts, read from its log. - `fusion_cancel_run` - STOP a library import that is in flight (fusion_install_library_bundle / fusion_create_library) - the user's escape hatch from a long run, wired to a Cancel...+- `fusion_check_dialogs` - List every Fusion dialog open now, each with its title, text and buttons read through UI Automation (no screenshot needed). - `fusion_check_recovery` - Ask whether a cloud file has a crash-recovery document waiting, which would raise Fusion's recovery modal on open. - `fusion_close_all_documents` - Close ALL open documents without save dialogs. - `fusion_close_document` - Close ONE document WITHOUT the modal save dialog (save:false discards silently - the automation-safe close). - `fusion_close_window` - Close a DIALOG window by hwnd (WM_CLOSE, like clicking X) - for dialogs without Cancel/Escape handling (e.g. Recovered Documents).+- `fusion_dialog_log` - The EDA Bridge Standard's name for fusion_action_ledger: the same verb, same arguments and reply (marked aliasOf). - `fusion_dismiss_blocking_dialogs` - Detect + classify (and optionally dismiss) blocking Fusion dialogs. - `fusion_dismiss_dialogs` - The EDA Bridge Standard's name for fusion_dismiss_blocking_dialogs: the same verb, same arguments and reply (marked aliasOf). - `fusion_notify_owner` - LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-ab to every peer on the relay, so it reaches them even when this bridge runs on an...@@ -739,6 +746,8 @@ _202 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. - `fusion_layer_stack` - The board's stack-up in the background, from Fusion's own Layer Stack Manager data in the board: copper layers top to bottom (F.Cu, In1.Cu, ..., B.Cu) with... - `fusion_layers` - The board's layer set: id, name (copper by the standard's names, the rest by Fusion's own: SilkscreenTop, BoardOutline...), kind (copper, silkscreen, mask... - `fusion_layers_in_use` - Which layers actually carry something: copper layers with copper (most first) and every layer with any primitive (board, footprints on their real side...+- `fusion_license_status` - The EDA Bridge Standard's name for fusion_account: the same verb, same arguments and reply (marked aliasOf).+- `fusion_list_commands` - The EDA Bridge Standard's name for fusion_list_text_commands: the same verb, same arguments and reply (marked aliasOf). - `fusion_list_designators` - Every part designator on the live board, in natural order (C2 before C11). - `fusion_list_nets` - Every net on the live board with its pad, track, arc, via and pour counts and its net class. - `fusion_list_text_commands` - List Fusion's internal TEXT COMMANDS (TextCommands.List) whose name contains filter, case-insensitive; the discovery tool for fusion_execute_text_command.@@ -758,13 +767,24 @@ _202 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. - `fusion_prewarm` - Warm the bridge ahead of the first real call, WITHOUT starting Fusion: confirms the add-in is synced into every Fusion add-in folder, readies the space-free... - `fusion_read_rules` - Every design rule on the live board from Fusion's rule manager, disabled ones included: name, kind (Copper Clearance, Minimum Copper Width, Teardrop...)... - `fusion_read_sheet_file` - Read a schematic straight from its file, without Fusion running: every part of an EAGLE .sch (or the .sch inside a Fusion .fsch) with its designator, library...+- `fusion_remove_items` - Delete tracks and vias given exactly as fusion_routing_state returned them (track start, end, layer; via position). - `fusion_remove_pours` - Delete pours (all of them, or the ones named by net, layer and outline) so a board can be poured again after a refine. - `fusion_repour` - Refill every copper pour on the live board (Fusion's ratsnest pass, with hidden fills shown again) and read the poured copper back: filled area per layer and... - `fusion_ripup_nets` - Take whole nets' tracks and vias back up (RIPUP per net, one undo step each) so they can be routed again, wider or another way. - `fusion_rules_by_kind` - The ENABLED design rules grouped by kind, each group in priority order.+- `fusion_run_command` - The EDA Bridge Standard's name for fusion_execute_text_command: the same verb, same arguments and reply (marked aliasOf). - `fusion_run_drc` - Run Fusion's own design rule check on the live board in the background and read every violation back: rule text, error or warning, layer, position in mm...+- `fusion_run_erc` - Fusion's own ERC on the open board's schematic: the design's schematic tab is brought forward for the check and the board's put back; every message is read...+- `fusion_run_script` - The EDA Bridge Standard's name for fusion_run_modeling_script: the same verb, same arguments and reply (marked aliasOf).+- `fusion_save` - The EDA Bridge Standard's name for fusion_save_document: the same verb, same arguments and reply (marked aliasOf).+- `fusion_select_net` - Highlight one whole net on the live board (every pad, track, via and pour of it) and, with zoom, frame it.+- `fusion_set_layer_visibility` - Show or hide one layer of the live 2D board (DISPLAY), read back from the board's own layer flags.+- `fusion_set_outline` - Replace the board outline (Fusion's Dimension layer 20, its board edge in the CAM output) with a rectangle, rounded corners optional, or a polygon (mm). - `fusion_set_preference` - Set Fusion appearance/navigation preferences in the LIVE session - applies immediately, NO restart.+- `fusion_set_track_width` - Change the width of the existing tracks on the given nets or net class, optionally one copper layer.+- `fusion_show_3d_chip` - The EDA Bridge Standard's name for fusion_show_3d_package: the same verb, same arguments and reply (marked aliasOf). - `fusion_silk_state` - The silkscreen as it stands: every part's body box, its designator and value texts (position, size, rotation, side, shown; a smashed part's own text, else the...+- `fusion_status` - The EDA Bridge Standard's name for fusion_readiness: the same verb, same arguments and reply (marked aliasOf). - `fusion_zoom_to` - Frame Fusion's board view on a rectangle, a part (or several) or a whole net, with a margin. ## Two install layers (they are NOT the same thing)@@ -791,7 +811,7 @@ adom-wiki pkg install adom/fusion-bridge # the skills pack for your AI Auto-updates via `updateManifestUrl` (ab polls on launch + every 4h). Every release lands on the insiders manifest first, and the public manifest moves only after the pre-production gate, so the-two tiers can serve different versions. 202 verbs -+two tiers can serve different versions. 222 verbs - `fusion_describe '{}'` returns the full machine-readable catalog. More demos: <video controls width="100%" src="https://wiki.adom.inc/api/v1/pages/fusion-bridge/files/library2-3d-montage.mp4"></video>
bridge.json+20@@ -47,10 +47,13 @@ "fusion_aps_versions", "fusion_assembly_bom", "fusion_attach_3d_package",+ "fusion_autoroute", "fusion_bind_library_3d", "fusion_board_bom", "fusion_board_info",+ "fusion_board_outline", "fusion_board_seed",+ "fusion_board_shape", "fusion_board_source", "fusion_board_stackup", "fusion_board_summary",@@ -59,6 +62,7 @@ "fusion_cancel_run", "fusion_capture_library_views", "fusion_change_pours",+ "fusion_check_dialogs", "fusion_check_recovery", "fusion_claim_seat", "fusion_cleanup_cloud_files",@@ -75,6 +79,7 @@ "fusion_demo_progress", "fusion_describe", "fusion_detect_layers",+ "fusion_dialog_log", "fusion_dismiss_blocking_dialogs", "fusion_dismiss_dialogs", "fusion_document_info",@@ -127,8 +132,10 @@ "fusion_layers_in_use", "fusion_library_manager", "fusion_library_parts",+ "fusion_license_status", "fusion_list_cloud_files", "fusion_list_cloud_projects",+ "fusion_list_commands", "fusion_list_designators", "fusion_list_nets", "fusion_list_text_commands",@@ -158,6 +165,7 @@ "fusion_open_by_urn_note", "fusion_open_cloud_file", "fusion_open_design",+ "fusion_open_document", "fusion_open_electronics", "fusion_open_lbr", "fusion_open_schematic",@@ -175,6 +183,7 @@ "fusion_read_sheet_file", "fusion_readiness", "fusion_release_seat",+ "fusion_remove_items", "fusion_remove_pours", "fusion_repour", "fusion_reset_workspace",@@ -185,8 +194,12 @@ "fusion_routing_drc", "fusion_routing_state", "fusion_rules_by_kind",+ "fusion_run_command", "fusion_run_drc",+ "fusion_run_erc", "fusion_run_modeling_script",+ "fusion_run_script",+ "fusion_save", "fusion_save_document", "fusion_save_to_cloud", "fusion_schematic",@@ -194,15 +207,20 @@ "fusion_schematic_run", "fusion_screenshot_fusion", "fusion_search_cloud_files",+ "fusion_select_net", "fusion_set_auto_update", "fusion_set_design_rules", "fusion_set_driving_badge",+ "fusion_set_layer_visibility",+ "fusion_set_outline", "fusion_set_parameter", "fusion_set_preference", "fusion_set_seat_takeover",+ "fusion_set_track_width", "fusion_set_window_sizing", "fusion_show_2d_board", "fusion_show_3d_board",+ "fusion_show_3d_chip", "fusion_show_3d_package", "fusion_show_attributes", "fusion_show_footprint",@@ -212,9 +230,11 @@ "fusion_show_symbol", "fusion_signin", "fusion_signin_2fa",+ "fusion_signin_status", "fusion_silk_state", "fusion_start", "fusion_state",+ "fusion_status", "fusion_stop", "fusion_take_screenshot", "fusion_uninstall",
contracts/describe.json+382@@ -491,6 +491,35 @@ "summary": "LOW-LEVEL primitive: attaches ONE model to ONE package. Prefer fusion_create_library with parts[]. Attach a real 3D chip model to a library package, end to end: opens the .lbr (library active), runs Electron.Create3DPackage to enter the Package3DEnvironment showing the footprint, imports the STEP + auto-orients it flat on the footprint, then executes Package3DStop (FINISH). This BINDS the 3D onto the deviceset (Content Manager shows the 3D nested under the package; the Packages 'Package' column flips Placeholder->part-name). ONE desktop-side step remains: Fusion pops a modal Save dialog (an OWNED popup) - the response _hint gives the exact desktop_ui_click to confirm it. AFTER: re-grab desktop_screenshot_window and check ownedPopupCount for errors; the 3D preview LAGS a beat before showing the real chip. Both paths (filePath/modelPath) are WINDOWS paths - stage from the container first.", "timeoutSeconds": 240 },+ {+ "example": {+ "nets": [+ "SDA",+ "SCL"+ ]+ },+ "input": {+ "drc": "bool, default true",+ "expectedRevision": "str",+ "lockPreroutes": "bool, default true (Fusion's autorouter always keeps existing tracks)",+ "nets": "[str] (default: every net with unconnected copper)"+ },+ "longRunning": true,+ "name": "fusion_autoroute",+ "output": {+ "added": "{tracks, vias}",+ "after": "{unconnected, errors}",+ "before": "{unconnected, errors}",+ "nets": "[str]",+ "revision": "str",+ "routed": "[str]",+ "success": "bool",+ "unrouted": "[str]"+ },+ "statusVerb": null,+ "summary": "Fusion's autorouter on the given nets, or on every net with unconnected copper. Existing tracks are kept (read back, or the run is undone); Fusion's DRC runs before and after. Refused before anything runs when a copper layer in use is switched off in the board's autorouter setup (Fusion would stop to ask): the reply names the layers.",+ "timeoutSeconds": 600+ }, { "example": { "libraryPath": "C:/Temp/PART.lbr",@@ -545,6 +574,22 @@ "summary": "Raw board geometry XML (copper/arcs/lines) for the open board.", "timeoutSeconds": 60 },+ {+ "example": {},+ "input": {},+ "longRunning": false,+ "name": "fusion_board_outline",+ "output": {+ "alreadyThere": "int",+ "drawn": "0",+ "layer": "str",+ "outlineClosed": "bool",+ "success": "bool"+ },+ "statusVerb": null,+ "summary": "Report the board edge the fab gets. Fusion's edge is its Dimension layer (20) and its Gerbers export it as the outline, so there is nothing to draw: this says how many outline items are there and whether they close. Change the shape with fusion_set_outline.",+ "timeoutSeconds": 60+ }, { "example": {}, "input": {@@ -567,6 +612,27 @@ "summary": "Write the design a new board starts from, as EAGLE source on this machine: the board outline, the fab's design rules (Fusion's own rules block with your clearance, edge distance, minimum width and drill, hole-to-hole, pad and via restring and copper weight; Fusion's defaults are 0.35 mm drill, 1 mm to the edge and a 25 % pad restring, wrong for most fabs), the fixed parts at their places with their nets, and the library they come from. Then fusion_new_electronics_from_eagle imports it as a linked schematic and board.", "timeoutSeconds": 60 },+ {+ "example": {+ "hMm": 25,+ "wMm": 40+ },+ "input": {+ "expectedRevision": "str",+ "hMm": "float, default 15",+ "wMm": "float, default 15",+ "x0Mm": "float, default 0",+ "y0Mm": "float, default 0"+ },+ "longRunning": false,+ "name": "fusion_board_shape",+ "output": {+ "...": "as fusion_set_outline"+ },+ "statusVerb": null,+ "summary": "A rectangular board outline from its corner and size: fusion_set_outline {outline:{rect:[x0Mm,y0Mm,wMm,hMm]}}.",+ "timeoutSeconds": 180+ }, { "example": {}, "input": {@@ -739,6 +805,20 @@ "summary": "Change existing pours in place: the pen width (the narrowest copper the fill can reach; a pen wider than a throat leaves it empty) and the isolation. Each pour is named by net, layer and outline, picked on the midpoint of its longest edge, then RATSNEST refills. The readback proves each pour took its new width/isolate and that no track or via changed, and reports the filled area before and after.", "timeoutSeconds": 120 },+ {+ "example": {},+ "input": {},+ "longRunning": false,+ "name": "fusion_check_dialogs",+ "output": {+ "count": "int",+ "dialogs": "[{hwnd, title, text, buttons}]",+ "success": "bool"+ },+ "statusVerb": null,+ "summary": "List every Fusion dialog open now, each with its title, text and buttons read through UI Automation (no screenshot needed). The bridge expires unknown dialogs by itself before the next add-in verb; in-canvas alerts that have no window are in fusion_alerts.",+ "timeoutSeconds": 30+ }, { "example": { "fileName": "BQ25792",@@ -1069,6 +1149,21 @@ "summary": "Detect 2-layer vs 4-layer board (ULP + CAM comparison).", "timeoutSeconds": 90 },+ {+ "example": {},+ "input": {+ "...": "as fusion_action_ledger"+ },+ "longRunning": false,+ "name": "fusion_dialog_log",+ "output": {+ "...": "as fusion_action_ledger",+ "aliasOf": "fusion_action_ledger"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_action_ledger: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ }, { "example": {}, "input": {@@ -2059,6 +2154,21 @@ "summary": "Every ORDERABLE variant in a .lbr with its part numbers - offline, no Fusion needed. For an EE this is the point of the library: two variants can share ONE symbol, ONE footprint and ONE 3D body and still be different products (an AEC-Q100 automotive part vs the baseline one), and that difference lives ENTIRELY in the metadata. EAGLE spells the orderable identity as deviceset + device + technology: <device> carries the PACKAGE variant, <technology> carries the GRADE/temperature variant, and attributes hang off <technology>. Reports per-variant MPN / MANUFACTURER / MOUSER_PN / DIGIKEY_PN / LCSC_PN (the exact names this bridge's own BOM export reads), plus warnings when variants are indistinguishable, share an MPN, or a deviceset has no description. These warnings never block the library from opening - they say whether an EE could actually order from it.", "timeoutSeconds": 15 },+ {+ "example": {},+ "input": {+ "...": "as fusion_account"+ },+ "longRunning": false,+ "name": "fusion_license_status",+ "output": {+ "...": "as fusion_account",+ "aliasOf": "fusion_account"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_account: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ }, { "example": { "projectName": "Main"@@ -2090,6 +2200,21 @@ "summary": "List the cloud PROJECTS visible to the signed-in Fusion session (hub walk - slow on big teams; prefer fusion_aps_search for finding files).", "timeoutSeconds": 120 },+ {+ "example": {},+ "input": {+ "...": "as fusion_list_text_commands"+ },+ "longRunning": false,+ "name": "fusion_list_commands",+ "output": {+ "...": "as fusion_list_text_commands",+ "aliasOf": "fusion_list_text_commands"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_list_text_commands: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ }, { "example": { "expectDocument": "fusion-routing"@@ -2643,6 +2768,21 @@ "summary": "Open a LOCAL design file (routes through the add-in import). Prefer cloud flows (fusion_open_cloud_file) for team designs; this is for local .f3d/.step style files.", "timeoutSeconds": 240 },+ {+ "example": {},+ "input": {+ "...": "as fusion_open_design"+ },+ "longRunning": false,+ "name": "fusion_open_document",+ "output": {+ "...": "as fusion_open_design",+ "aliasOf": "fusion_open_design"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_open_design: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ }, { "example": {}, "input": {@@ -2991,6 +3131,35 @@ "summary": "Give up the Autodesk seat from THIS machine so another box can claim it with no modal at all: closes Fusion cleanly (documents are NOT discarded; an unsaved one refuses and says so) and leaves the account signed in, so this box can claim the seat straight back. The counterpart of fusion_claim_seat.", "timeoutSeconds": 180 },+ {+ "example": {+ "vias": [+ {+ "position": {+ "x": 12.5,+ "y": 8+ }+ }+ ]+ },+ "input": {+ "expectedRevision": "str from fusion_routing_state",+ "tracks": "[{start:{x,y}, end:{x,y}, layer}]",+ "vias": "[{position:{x,y}}]"+ },+ "longRunning": false,+ "name": "fusion_remove_items",+ "output": {+ "removed": "{tracks, vias}",+ "results": "[str]",+ "revision": "str",+ "success": "bool",+ "unconnected": "int"+ },+ "statusVerb": null,+ "summary": "Delete tracks and vias given exactly as fusion_routing_state returned them (track start, end, layer; via position). The board is read back as the old one minus exactly those items, or every removal is undone. results[] is per item: ok or err:not found.",+ "timeoutSeconds": 180+ }, { "example": {}, "input": {@@ -3203,6 +3372,21 @@ "summary": "The ENABLED design rules grouped by kind, each group in priority order.", "timeoutSeconds": 60 },+ {+ "example": {},+ "input": {+ "...": "as fusion_execute_text_command"+ },+ "longRunning": false,+ "name": "fusion_run_command",+ "output": {+ "...": "as fusion_execute_text_command",+ "aliasOf": "fusion_execute_text_command"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_execute_text_command: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ }, { "example": { "expectDocument": "fusion-routing"@@ -3226,6 +3410,26 @@ "summary": "Run Fusion's own design rule check on the live board in the background and read every violation back: rule text, error or warning, layer, position in mm, approved or not, plus counts by rule. No revision guard: it changes no copper, parts or outline, but Fusion stores the DRC result in the board, so the document then reads as modified (close it without saving if you only looked). fusion_routing_drc is the same check inside the routing guards.", "timeoutSeconds": 120 },+ {+ "example": {},+ "input": {+ "expectDocument": "the board's name"+ },+ "longRunning": false,+ "name": "fusion_run_erc",+ "output": {+ "byClass": "{error, warning}",+ "byCode": "{code: count}",+ "errorCount": "int",+ "messageCount": "int",+ "messages": "[{class, source, document, text, code, sheet, x, y, approved}]",+ "success": "bool",+ "warningCount": "int"+ },+ "statusVerb": null,+ "summary": "Fusion's own ERC on the open board's schematic: the design's schematic tab is brought forward for the check and the board's put back; every message is read from Fusion (UL_SCHEMATIC.errors) with its class, code, sheet and position. No dialog.",+ "timeoutSeconds": 120+ }, { "example": { "script": "import adsk.fusion\n# ...build geometry..."@@ -3243,6 +3447,36 @@ "summary": "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.", "timeoutSeconds": 180 },+ {+ "example": {},+ "input": {+ "...": "as fusion_run_modeling_script"+ },+ "longRunning": false,+ "name": "fusion_run_script",+ "output": {+ "...": "as fusion_run_modeling_script",+ "aliasOf": "fusion_run_modeling_script"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_run_modeling_script: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ },+ {+ "example": {},+ "input": {+ "...": "as fusion_save_document"+ },+ "longRunning": false,+ "name": "fusion_save",+ "output": {+ "...": "as fusion_save_document",+ "aliasOf": "fusion_save_document"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_save_document: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ }, { "example": { "comment": "routed the power nets"@@ -3395,6 +3629,29 @@ "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": {+ "net": "VBUS",+ "zoom": true+ },+ "input": {+ "margin": "mm around the net when zooming, default 1",+ "net": "str (omit to clear)",+ "zoom": "bool, default false"+ },+ "longRunning": false,+ "name": "fusion_select_net",+ "output": {+ "byKind": "{tracks, arcs, vias, pads, pours, other}",+ "net": "str",+ "selected": "int",+ "success": "bool",+ "zoom": "[x0,y0,x1,y1] mm"+ },+ "statusVerb": null,+ "summary": "Highlight one whole net on the live board (every pad, track, via and pour of it) and, with zoom, frame it. No net clears the highlight. Counts what was highlighted by kind.",+ "timeoutSeconds": 60+ }, { "example": { "enabled": true@@ -3455,6 +3712,58 @@ "summary": "Read/toggle the 'Adom is driving Fusion' TASKBAR OVERLAY BADGE (STICKY per user, default ON). While the bridge drives Fusion, its Windows taskbar button wears a small Adom mark so the user knows at a glance who is in control; it sheds automatically after 5 minutes of driving-verb idle. No args = read. Turning it off clears any badge immediately.", "timeoutSeconds": 15 },+ {+ "example": {+ "displayed": false,+ "layerId": "B.Cu"+ },+ "input": {+ "designName": "optional str: the board to act on",+ "displayed": "bool, default true",+ "layerId": "required int | str (alias: layer)"+ },+ "longRunning": false,+ "name": "fusion_set_layer_visibility",+ "output": {+ "displayed": "bool",+ "layerId": "int",+ "name": "str",+ "revision": "str",+ "success": "bool"+ },+ "statusVerb": null,+ "summary": "Show or hide one layer of the live 2D board (DISPLAY), read back from the board's own layer flags. Layer by id (fusion_layers), copper name (F.Cu, In1.Cu, B.Cu) or top/bottom. Nothing on the board changes.",+ "timeoutSeconds": 60+ },+ {+ "example": {+ "outline": {+ "cornerRadius": 2,+ "rect": [+ 0,+ 0,+ 50,+ 30+ ]+ }+ },+ "input": {+ "expectedRevision": "str",+ "outline": "{rect:[x0,y0,w,h], cornerRadius?} | {points:[[x,y]...]}"+ },+ "longRunning": false,+ "name": "fusion_set_outline",+ "output": {+ "outlineEdges": "[edge]",+ "replaced": "int",+ "revision": "str",+ "success": "bool",+ "vertices": "int"+ },+ "statusVerb": null,+ "summary": "Replace the board outline (Fusion's Dimension layer 20, its board edge in the CAM output) with a rectangle, rounded corners optional, or a polygon (mm). The old outline lines are deleted, the new ones drawn, and the closed outline read back; if any copper moved, every command is undone.",+ "timeoutSeconds": 180+ }, { "example": { "expression": "1.6 mm",@@ -3515,6 +3824,34 @@ "summary": "Read/toggle the mid-session Autodesk seat-takeover sweep (STICKY per user, default ON). When Autodesk pops 'Active Sessions Exceeded' mid-session, the bridge clicks Continue through BOTH dialogs in the background: this machine takes the seat, the other machine's Fusion suspends RESUMABLY, the user gets one toast. No args = read current value. Gates only the passive sweep - a verb-driven fusion_start still grabs the seat (the user asked for Fusion, so the seat is implied).", "timeoutSeconds": 15 },+ {+ "example": {+ "nets": [+ "VBUS"+ ],+ "widthMm": 0.5+ },+ "input": {+ "designName": "optional str",+ "layer": "optional layer id or name",+ "netClass": "str",+ "nets": "[str]",+ "widthMm": "required float"+ },+ "longRunning": false,+ "name": "fusion_set_track_width",+ "output": {+ "byNet": "{net: count}",+ "changed": "int",+ "notOnBoard": "[str]",+ "revision": "str",+ "success": "bool",+ "widthMm": "float"+ },+ "statusVerb": null,+ "summary": "Change the width of the existing tracks on the given nets or net class, optionally one copper layer. Each track is picked with only its layer displayed (the display is restored), then the copper is read back: only the asked tracks may change, and only their width, or every change is undone. Arcs are left alone.",+ "timeoutSeconds": 180+ }, { "example": {}, "input": {@@ -3561,6 +3898,21 @@ "summary": "Switch the OPEN electronics design to its 3D PCB view (populated board). A VIEW of the project, not a standalone file. Orbit it for demos via fusion_run_modeling_script (app.activeViewport.camera + vp.refresh()).", "timeoutSeconds": 70 },+ {+ "example": {},+ "input": {+ "...": "as fusion_show_3d_package"+ },+ "longRunning": false,+ "name": "fusion_show_3d_chip",+ "output": {+ "...": "as fusion_show_3d_package",+ "aliasOf": "fusion_show_3d_package"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_show_3d_package: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ }, { "example": { "name": "MYCHIP",@@ -3767,6 +4119,21 @@ "summary": "Submit Autodesk's emailed 6-DIGIT verification code to finish a Fusion sign-in, typed IN THE BACKGROUND (UIA SetValue + Invoke - never steals the user's foreground or moves their cursor). Exists so you can finish a login END TO END for the user instead of parking them on a 2FA screen: if `adom-google` is available in your container, read the code out of their Gmail (`adom-google gmail search 'from:autodesk verification code' --limit 1`), pull the 6 digits, and call this. If adom-google is NOT installed, tell the user that installing it would let you automate logins like this, then let them type it. NEVER ask the user for a code you could read yourself. The bridge ALSO toasts the user's desktop whenever it hits a human wall, so you do not need to ask it to.", "timeoutSeconds": 90 },+ {+ "example": {},+ "input": {+ "...": "as fusion_account"+ },+ "longRunning": false,+ "name": "fusion_signin_status",+ "output": {+ "...": "as fusion_account",+ "aliasOf": "fusion_account"+ },+ "statusVerb": null,+ "summary": "The EDA Bridge Standard's name for fusion_account: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ "timeoutSeconds": 120+ }, { "example": { "expectDocument": "fusion-routing"@@ -3827,6 +4194,21 @@ "summary": "The RENDER-ME verb (kicad_state parity, issues #28/#30): running + window + dialog + busy state PLUS a fresh background screenshot of whatever Fusion is showing - the one verb that makes every background fusion_show_* result visible. SERVER-SIDE ONLY by design: it answers even while the add-in main thread is busy or wedged (activeDocument is best-effort with a short budget and skipped when busy). blockingDialogs carries the CLASSIFIED modal list (cause + resolution). imageAs:base64 inlines the PNG (auto-refused over 4MB - pull the screenshot path via pull_file instead).", "timeoutSeconds": 60 },+ {+ "example": {},+ "input": {+ "...": "as fusion_readiness"+ },+ "longRunning": false,+ "name": "fusion_status",+ "output": {+ "...": "as fusion_readiness",+ "aliasOf": "fusion_readiness"+ },+ "statusVerb": null,+ "summary": "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.",+ "timeoutSeconds": 120+ }, { "example": {}, "input": {
describe.py+51−1@@ -85,6 +85,36 @@ _T = [ ('fusion_zoom_to', "Frame Fusion's board view on a rectangle, a part (or several) or a whole net, with a margin. Inside Fusion only: no window focus or mouse.", {"expectDocument":"required exact active board name","boardId":"optional","rect":"optional [x1,y1,x2,y2] mm","ref":"optional designator","refs":"optional [designator]","net":"optional net","margin":"mm, default 1"}, {"rect":"[x0,y0,x1,y1] mm framed"},60,"fusion_routing_state",False,{"expectDocument":"fusion-routing"}),+ ("fusion_check_dialogs", "List every Fusion dialog open now, each with its title, text and buttons read through UI Automation (no screenshot needed). The bridge expires unknown dialogs by itself before the next add-in verb; in-canvas alerts that have no window are in fusion_alerts.",+ {}, {"success": "bool", "count": "int", "dialogs": "[{hwnd, title, text, buttons}]"}, 30, None, False, {}),+ ("fusion_set_layer_visibility", "Show or hide one layer of the live 2D board (DISPLAY), read back from the board's own layer flags. Layer by id (fusion_layers), copper name (F.Cu, In1.Cu, B.Cu) or top/bottom. Nothing on the board changes.",+ {"layerId": "required int | str (alias: layer)", "displayed": "bool, default true", "designName": "optional str: the board to act on"},+ {"success": "bool", "layerId": "int", "name": "str", "displayed": "bool", "revision": "str"}, 60, None, False, {"layerId": "B.Cu", "displayed": False}),+ ("fusion_select_net", "Highlight one whole net on the live board (every pad, track, via and pour of it) and, with zoom, frame it. No net clears the highlight. Counts what was highlighted by kind.",+ {"net": "str (omit to clear)", "zoom": "bool, default false", "margin": "mm around the net when zooming, default 1"},+ {"success": "bool", "net": "str", "selected": "int", "byKind": "{tracks, arcs, vias, pads, pours, other}", "zoom": "[x0,y0,x1,y1] mm"}, 60, None, False, {"net": "VBUS", "zoom": True}),+ ("fusion_set_track_width", "Change the width of the existing tracks on the given nets or net class, optionally one copper layer. Each track is picked with only its layer displayed (the display is restored), then the copper is read back: only the asked tracks may change, and only their width, or every change is undone. Arcs are left alone.",+ {"nets": "[str]", "netClass": "str", "layer": "optional layer id or name", "widthMm": "required float", "designName": "optional str"},+ {"success": "bool", "changed": "int", "byNet": "{net: count}", "notOnBoard": "[str]", "widthMm": "float", "revision": "str"}, 180, None, False, {"nets": ["VBUS"], "widthMm": 0.5}),+ ("fusion_remove_items", "Delete tracks and vias given exactly as fusion_routing_state returned them (track start, end, layer; via position). The board is read back as the old one minus exactly those items, or every removal is undone. results[] is per item: ok or err:not found.",+ {"expectedRevision": "str from fusion_routing_state", "tracks": "[{start:{x,y}, end:{x,y}, layer}]", "vias": "[{position:{x,y}}]"},+ {"success": "bool", "results": "[str]", "removed": "{tracks, vias}", "unconnected": "int", "revision": "str"}, 180, None, False, {"vias": [{"position": {"x": 12.5, "y": 8}}]}),+ ("fusion_set_outline", "Replace the board outline (Fusion's Dimension layer 20, its board edge in the CAM output) with a rectangle, rounded corners optional, or a polygon (mm). The old outline lines are deleted, the new ones drawn, and the closed outline read back; if any copper moved, every command is undone.",+ {"expectedRevision": "str", "outline": "{rect:[x0,y0,w,h], cornerRadius?} | {points:[[x,y]...]}"},+ {"success": "bool", "outlineEdges": "[edge]", "vertices": "int", "replaced": "int", "revision": "str"}, 180, None, False, {"outline": {"rect": [0, 0, 50, 30], "cornerRadius": 2}}),+ ("fusion_board_shape", "A rectangular board outline from its corner and size: fusion_set_outline {outline:{rect:[x0Mm,y0Mm,wMm,hMm]}}.",+ {"x0Mm": "float, default 0", "y0Mm": "float, default 0", "wMm": "float, default 15", "hMm": "float, default 15", "expectedRevision": "str"},+ {"...": "as fusion_set_outline"}, 180, None, False, {"wMm": 40, "hMm": 25}),+ ("fusion_board_outline", "Report the board edge the fab gets. Fusion's edge is its Dimension layer (20) and its Gerbers export it as the outline, so there is nothing to draw: this says how many outline items are there and whether they close. Change the shape with fusion_set_outline.",+ {}, {"success": "bool", "layer": "str", "alreadyThere": "int", "drawn": "0", "outlineClosed": "bool"}, 60, None, False, {}),+ ("fusion_run_erc", "Fusion's own ERC on the open board's schematic: the design's schematic tab is brought forward for the check and the board's put back; every message is read from Fusion (UL_SCHEMATIC.errors) with its class, code, sheet and position. No dialog.",+ {"expectDocument": "the board's name"},+ {"success": "bool", "messages": "[{class, source, document, text, code, sheet, x, y, approved}]", "messageCount": "int", "byClass": "{error, warning}", "byCode": "{code: count}", "errorCount": "int", "warningCount": "int"},+ 120, None, False, {}),+ ("fusion_autoroute", "Fusion's autorouter on the given nets, or on every net with unconnected copper. Existing tracks are kept (read back, or the run is undone); Fusion's DRC runs before and after. Refused before anything runs when a copper layer in use is switched off in the board's autorouter setup (Fusion would stop to ask): the reply names the layers.",+ {"nets": "[str] (default: every net with unconnected copper)", "lockPreroutes": "bool, default true (Fusion's autorouter always keeps existing tracks)", "drc": "bool, default true", "expectedRevision": "str"},+ {"success": "bool", "nets": "[str]", "routed": "[str]", "unrouted": "[str]", "added": "{tracks, vias}", "before": "{unconnected, errors}", "after": "{unconnected, errors}", "revision": "str"},+ 600, None, True, {"nets": ["SDA", "SCL"]}), ("fusion_apply_silkscreen", "Land a silkscreen plan on the live board: designators (names) and values (comments) moved to their own location, sized, turned or hidden; free silkscreen texts and lines added (bottom ones mirrored); earlier free items removed by the raw handle fusion_silk_state gives. Parts not yet smashed are smashed first. Each pick is made with only the target's layer displayed, so a pad or wire is never taken instead, and the board's layer display is restored. Read back; if copper, parts or the outline moved, every command is undone. The edit side of a designator plan (silk_designators).", {"expectDocument":"required exact board name","expectedRevision":"optional: refuse with stale_board if the board moved on","names":"[{ref, x, y, size?, rotation?, show?}] the designator text's own location (lower left before rotation), mm","comments":"[same] for the value text","texts":"[{text, x, y, size?, rotation?, side?: top|bottom}]","lines":"[{x1, y1, x2, y2, width? (0.15), side?}]","remove":"{texts:[raw], lines:[raw]} from fusion_silk_state"}, {"applied":"int","failed":"int","results":"[ok | err:...]","smashed":"[ref]","after":"silk_state","revision":"str"},120,"fusion_routing_state",False,{"expectDocument":"fusion-routing","names":[{"ref":"J1","x":10,"y":20,"size":1.5,"rotation":90}]}),@@ -379,7 +409,7 @@ _T = [ ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}), ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}), ("fusion_export_usdz", "Export to USDZ (AR / Hydrogen 3D).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.usdz"}),- ("fusion_export_optimized_glb", "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d. STACKUP RULE (John 2026-07-15, see pcb-stackup skill): board bbox = the FR4 body, NEVER the whole assembly (components overhang edges + tower above); each overlay goes at ITS layer's z (copper at FR4-top, silk above mask); copper traces are REAL 35um 3D bodies colored like the FR4 - recolor them + translucent mask in the RAW GLB before optimizing to make routing visible; put the measured stackup table on the board's wiki page. PUBLISH EVERYTHING: a board's wiki page repo must carry EVERY exportable design file (.f3d/.step/.usdz/EAGLE .brd+.sch/Fusion .fbrd+.fsch/gerbers/BOM/CPL/GLBs/renders), not just the 3D - the page exists to share the design.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)", "async": "bool - RECOMMENDED true: fires the whole export+optimize pipeline in the background and returns {exportId} instantly (the sync form can outlive the relay window on a cold Fusion session); poll fusion_fetch_optimized_glb {exportId}"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}),+ ("fusion_export_optimized_glb", "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d. STACKUP RULE (John 2026-07-15, see pcb-stackup skill): board bbox = the FR4 body, NEVER the whole assembly (components overhang edges + tower above); each overlay goes at ITS layer's z (copper at FR4-top, silk above mask); copper traces are REAL 35um 3D bodies colored like the FR4 - recolor them + translucent mask in the RAW GLB before optimizing to make routing visible; put the measured stackup table on the board's wiki page. PUBLISH EVERYTHING: a board's wiki page repo must carry EVERY exportable design file (.f3d/.step/.usdz/EAGLE .brd+.sch/Fusion .fbrd+.fsch/gerbers/BOM/CPL/GLBs/renders), not just the 3D - the page exists to share the design.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "compress": "draco (default) | quantize | none: none gives a plain GLB that wiki README and widget frames can load (they cannot reach a Draco decoder)", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)", "async": "bool - RECOMMENDED true: fires the whole export+optimize pipeline in the background and returns {exportId} instantly (the sync form can outlive the relay window on a cold Fusion session); poll fusion_fetch_optimized_glb {exportId}"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}), ("fusion_fetch_optimized_glb", "Finish a fusion_export_optimized_glb job: bounded-poll the optimizer by jobId and write the GLB to outputPath when complete. Re-call until status=complete (big boards tessellate a few minutes). No Fusion needed.", {"jobId": "str (service job form)", "exportId": "str (async form from export_optimized_glb async:true - returns {phase, done, glbPath} instantly per poll)", "outputPath": "str (.glb)", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "status": "str", "pending": "bool", "sizeBytes": "int"}, 150, None, False, {"jobId": "abc123", "outputPath": "C:/out/board.glb"}), ("fusion_board_stackup", "Read a board's PHYSICAL fabrication stackup (see the pcb-stackup skill): copper layer count + copper/dielectric thicknesses from the EAGLE design rules (layerSetup/mtCopper/mtIsolate) plus the measured FR4 extent. A PCB is Cu/prepreg/Cu/core/.../Cu - the FR4 is NOT one slab. Use to build the stackup table + real-thickness exploded 3D view, and put a stackup table on every exported board's wiki page.", {}, {"layerSetup": "str", "copperLayers": "list", "copperCount": "int", "copperThickness_mm": "list", "dielectricBonds": "list (prepreg/core order)", "fr4": "{x_mm,y_mm,dielectric_mm}"}, 200, None, True, {}), ("fusion_export_obj", "Export to OBJ mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.obj"}),@@ -540,6 +570,26 @@ _T = [ {"...":"as fusion_board_info"}, {"aliasOf":"fusion_board_info","...":"as fusion_board_info"}, 120, 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.",+ {"...":"as fusion_readiness"}, {"aliasOf":"fusion_readiness","...":"as fusion_readiness"}, 120, None, False, {}),+ ("fusion_save", "The EDA Bridge Standard's name for fusion_save_document: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_save_document"}, {"aliasOf":"fusion_save_document","...":"as fusion_save_document"}, 120, None, False, {}),+ ("fusion_run_script", "The EDA Bridge Standard's name for fusion_run_modeling_script: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_run_modeling_script"}, {"aliasOf":"fusion_run_modeling_script","...":"as fusion_run_modeling_script"}, 120, None, False, {}),+ ("fusion_run_command", "The EDA Bridge Standard's name for fusion_execute_text_command: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_execute_text_command"}, {"aliasOf":"fusion_execute_text_command","...":"as fusion_execute_text_command"}, 120, None, False, {}),+ ("fusion_list_commands", "The EDA Bridge Standard's name for fusion_list_text_commands: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_list_text_commands"}, {"aliasOf":"fusion_list_text_commands","...":"as fusion_list_text_commands"}, 120, None, False, {}),+ ("fusion_dialog_log", "The EDA Bridge Standard's name for fusion_action_ledger: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_action_ledger"}, {"aliasOf":"fusion_action_ledger","...":"as fusion_action_ledger"}, 120, None, False, {}),+ ("fusion_signin_status", "The EDA Bridge Standard's name for fusion_account: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_account"}, {"aliasOf":"fusion_account","...":"as fusion_account"}, 120, None, False, {}),+ ("fusion_license_status", "The EDA Bridge Standard's name for fusion_account: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_account"}, {"aliasOf":"fusion_account","...":"as fusion_account"}, 120, None, False, {}),+ ("fusion_open_document", "The EDA Bridge Standard's name for fusion_open_design: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_open_design"}, {"aliasOf":"fusion_open_design","...":"as fusion_open_design"}, 120, None, False, {}),+ ("fusion_show_3d_chip", "The EDA Bridge Standard's name for fusion_show_3d_package: the same verb, same arguments and reply (marked aliasOf). One name across Altium, KiCad and Fusion bridges.",+ {"...":"as fusion_show_3d_package"}, {"aliasOf":"fusion_show_3d_package","...":"as fusion_show_3d_package"}, 120, None, False, {}), ("fusion_describe", "Self-describe every verb the bridge exposes (this list) for ab's Verbs tab + runner. The reply carries `contribute` (source, mirror, issue and PR commands, the fusion-contributing skill); every failing reply carries `_contribute` with the same pointer.", {}, {"success": "bool", "verbs": "[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]"}, 20, None, False, {}), ]
rust/bridge/src/main.rs+4@@ -62,6 +62,10 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); 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"),+ ("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"),+ ("show_3d_chip", "fusion_show_3d_package"), ]; fn standard_alias(asked: &str) -> Option<&'static str> {
rust/bridge/src/routing.rs+2−2@@ -3,7 +3,7 @@ use serde_json::{json, Value}; const SOURCE: &str = concat!(include_str!("routing_script.py"), "\n", include_str!("routing_planner.py"), "\n", include_str!("board_model.py"), "\nFUSION_DEFAULT_RULES = r'''", include_str!("fusion_default_designrules.xml"), "'''\n");-pub fn owns(cmd: &str) -> bool { matches!(cmd, "routing_state" | "route_net" | "routing_drc" | "plan_route" | "move_parts" | "place_vias" | "apply_pours" | "pour_state" | "board_source" | "ripup_nets" | "schematic_run" | "board_bom" | "change_pours" | "schematic_plan" | "board_seed" | "remove_pours" | "layer_stack" | "netlist" | "bodies" | "component_data" | "component_pins" | "list_nets" | "net_connections" | "list_designators" | "layers" | "copper_layers" | "layers_in_use" | "read_rules" | "rules_by_kind" | "silk_state" | "net_classes" | "run_drc" | "repour" | "zoom_to" | "export_bom" | "save_document" | "save_to_cloud" | "apply_silkscreen") }+pub fn owns(cmd: &str) -> bool { matches!(cmd, "routing_state" | "route_net" | "routing_drc" | "plan_route" | "move_parts" | "place_vias" | "apply_pours" | "pour_state" | "board_source" | "ripup_nets" | "schematic_run" | "board_bom" | "change_pours" | "schematic_plan" | "board_seed" | "remove_pours" | "layer_stack" | "netlist" | "bodies" | "component_data" | "component_pins" | "list_nets" | "net_connections" | "list_designators" | "layers" | "copper_layers" | "layers_in_use" | "read_rules" | "rules_by_kind" | "silk_state" | "net_classes" | "run_drc" | "repour" | "zoom_to" | "export_bom" | "save_document" | "save_to_cloud" | "apply_silkscreen" | "set_layer_visibility" | "select_net" | "set_track_width" | "remove_items" | "set_outline" | "board_shape" | "board_outline" | "run_erc" | "autoroute") } pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value { let script = format!("import json\nexec({})\nresult=handle(app,json.loads({}),{})\n", serde_json::to_string(SOURCE).unwrap(),@@ -20,7 +20,7 @@ pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Val if !r["fusionUnexpectedCondition"].is_null() { out["_hint"] = json!(format!("{} Fusion Electronics raised its 'unexpected condition' assertion after this edit and the bridge clicked OK; the copper read-back above is the truth. Checkpoint now (fusion_export_eagle_source).", out["_hint"].as_str().unwrap_or("")).trim().to_string()); } out }- None => json!({"success":false,"errorCode":"invalid_routing_response","error":"Fusion did not return a structured routing result", "mutationMayHaveOccurred":matches!(cmd, "route_net" | "move_parts" | "place_vias" | "apply_pours")}),+ None => json!({"success":false,"errorCode":"invalid_routing_response","error":"Fusion did not return a structured routing result", "mutationMayHaveOccurred":matches!(cmd, "route_net" | "move_parts" | "place_vias" | "apply_pours" | "apply_silkscreen" | "set_track_width" | "remove_items" | "set_outline" | "board_shape" | "autoroute")}), } }
rust/bridge/src/routing_script.py+397@@ -916,6 +916,392 @@ def apply_silkscreen(app, root, board, meta, args): '_hint': 'Read back from the board: every result is ok or names what landed where. Each EAGLE command is its own undo step in Fusion (no single-step undo for a plan). Free texts and lines are removed by the raw handle silk_state gives.'} +def _visible_layers(root):+ return [l.get('number') for l in root.find('drawing/layers') if l.get('visible') == 'yes']+++def _restore_display(app, visible):+ """One layer per command (see apply_silkscreen: a DISPLAY that names the origin layers drops+ every layer after them)."""+ app.executeTextCommand('Electron.run DISPLAY NONE;')+ for n in visible:+ if n not in ('23', '24'):+ try: app.executeTextCommand('Electron.run DISPLAY %s;' % n)+ except Exception: pass+++def _layer_number(board, v):+ """A layer given as Fusion's number, the standard's copper name, or Fusion's own layer name."""+ if isinstance(v, int) and not isinstance(v, bool): return v+ if isinstance(v, str):+ if v.strip().isdigit(): return int(v)+ L = Layers(board)+ for n, nm in L.names.items():+ if nm.lower() == v.strip().lower(): return n+ if v.strip().lower() in ('top',): return 1+ if v.strip().lower() in ('bottom',): return 16+ fail('invalid_layer', 'Unknown layer %r: pass an id from fusion_layers, a copper name (F.Cu, In1.Cu, B.Cu) or top/bottom' % (v,))+++def set_layer_visibility(app, root, board, meta, args):+ n = _layer_number(board, args.get('layerId', args.get('layer')))+ if root.find("drawing/layers/layer[@number='%d']" % n) is None: fail('invalid_layer', 'This board has no layer %d' % n)+ on = args.get('displayed', True) is not False+ app.executeTextCommand('Electron.run DISPLAY %s%d;' % ('' if on else '-', n))+ app.executeTextCommand('Electron.ImmediateRepaint /full')+ root2, _, meta2 = snapshot(app, args)+ got = root2.find("drawing/layers/layer[@number='%d']" % n).get('visible') == 'yes'+ name = Layers(board).name(n) if n in COPPER else root2.find("drawing/layers/layer[@number='%d']" % n).get('name')+ return {'success': got == on, 'mutated': False, **meta2, 'layerId': n, 'name': name,+ 'displayed': got, 'errorCode': None if got == on else 'readback_mismatch', 'windowEtiquette': 'background'}+++def select_net(app, root, board, meta, args):+ """Highlight one whole net (EAGLE SHOW by name: every pad, track, via and pour of it) and frame it."""+ net = args.get('net')+ model, _ = board_model(board)+ if not net:+ app.executeTextCommand('Electron.run WINDOW;')+ return {'success': True, 'mutated': False, **meta, 'net': None, 'selected': 0, 'cleared': True}+ names = {n.lower(): n for n in model['nets']}+ if net.lower() not in names: fail('no_such_net', 'No net named %s' % net)+ net = identifier(names[net.lower()])+ by = {'tracks': sum(t['net'] == net for t in model['tracks']), 'arcs': sum(a['net'] == net for a in model['arcs']),+ 'vias': sum(v['net'] == net for v in model['vias']), 'pads': sum(p['net'] == net for p in model['pads']),+ 'pours': sum(g['net'] == net for g in model['polygons']), 'other': 0}+ out = {'success': True, 'mutated': False, **meta, 'net': net, 'byKind': by, 'selected': sum(by.values()), 'windowEtiquette': 'background'}+ # frame first: the highlight lasts until the next redraw+ if args.get('zoom'):+ pts = [(p['x'], p['y']) for p in model['pads'] if p['net'] == net] + [(t[k]['x'], t[k]['y']) for t in model['tracks'] if t['net'] == net for k in ('start', 'end')]+ if pts:+ m = number(args.get('margin', 1.0))+ box = [min(p[0] for p in pts) - m, min(p[1] for p in pts) - m, max(p[0] for p in pts) + m, max(p[1] for p in pts) + m]+ app.executeTextCommand('Electron.run WINDOW (%.4fmm %.4fmm) (%.4fmm %.4fmm);' % tuple(box))+ out['zoom'] = [round(v, 4) for v in box]+ # no closing ';': Fusion clears a finished SHOW's highlight at once, so the command is left+ # waiting for a click (a prompt in the command line, not a dialog); the next command ends it+ app.executeTextCommand("Electron.run SHOW '%s'" % net)+ return out+++def _undo_until(app, args, revision, tries):+ for _ in range(tries):+ app.executeTextCommand('Electron.run UNDO;')+ _, _, m = snapshot(app, args)+ if m['revision'] == revision: return True+ return False+++def _seg_dist(p, a, b):+ dx, dy = b[0] - a[0], b[1] - a[1]+ L2 = dx * dx + dy * dy+ t = 0.0 if not L2 else max(0.0, min(1.0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / L2))+ return math.hypot(p[0] - a[0] - t * dx, p[1] - a[1] - t * dy)+++def _pick_points(x, others, pads):+ """Points along wire x to pick it at, best first: farthest from the other wires on its layer+ and from pad centers (a midpoint pick took a neighbour: a stub into a pad, a wire end)."""+ out = []+ for t in (0.5, 0.35, 0.65, 0.2, 0.8, 0.1, 0.9):+ p = (x['start'][0] + t * (x['end'][0] - x['start'][0]), x['start'][1] + t * (x['end'][1] - x['start'][1]))+ d = min([_seg_dist(p, o['start'], o['end']) for o in others] + [math.hypot(p[0] - q[0], p[1] - q[1]) for q in pads] + [9.0])+ out.append((-round(d, 4), t, p))+ return [p for _, _, p in sorted(out)]+++def _wire_key(x):+ a, b = tuple(round(v, 4) for v in x['start']), tuple(round(v, 4) for v in x['end'])+ return (x['net'], x['layer'], min(a, b), max(a, b), round(x['curve'], 3))+++def set_track_width(app, root, board, meta, args):+ """Change the width of existing tracks on nets (or a net class), optionally one layer. Each+ wire is picked with only its layer displayed, at the point along it farthest from other copper,+ in up to three passes (each read back); then only the asked wires may differ, and only in+ width, or every change is undone. A wire no pick reaches (one lying on another) is reported."""+ w = number(args.get('widthMm', args.get('width')), True)+ nets, pads, wires, _ = objects(board)+ want = set(args.get('nets') or [])+ if args.get('netClass'):+ cls = {c.get('name').lower(): c.get('number') for c in board.findall('classes/class')}+ num = cls.get(str(args['netClass']).lower())+ if num is None: fail('no_such_class', 'No net class named %s' % args['netClass'])+ want |= {n for n, s_ in nets.items() if s_.get('class', '0') == num}+ if not want: fail('bad_args', 'Give nets or netClass')+ missing = sorted(n for n in want if n not in nets)+ layer = _layer_number(board, args['layer']) if args.get('layer') is not None else None+ targets = [x for x in wires if x['net'] in want and (layer is None or x['layer'] == layer) and not x['curve'] and abs(x['width'] - w) > 1e-6]+ tkeys = {_wire_key(x) for x in targets}+ before = {_wire_key(x): x['width'] for x in wires}+ visible = _visible_layers(root); n_cmd = 0; left = list(targets); meta2 = meta; after_wires = wires+ for attempt in range(3):+ if not left: break+ try:+ for x in left:+ others = [o for o in after_wires if o['layer'] == x['layer'] and _wire_key(o) != _wire_key(x)]+ pp = [p['at'] for p in pads.values() if x['layer'] in p['layers']]+ pts = _pick_points(x, others, pp)+ px, py = pts[min(attempt, len(pts) - 1)]+ app.executeTextCommand('Electron.run DISPLAY NONE %d;' % x['layer'])+ app.executeTextCommand('Electron.run CHANGE WIDTH %.6fmm (%.6fmm %.6fmm);' % (w, px, py)); n_cmd += 1+ finally:+ _restore_display(app, visible)+ _, after, meta2 = snapshot(app, args)+ after_wires = objects(after)[2]+ now = {_wire_key(x): x['width'] for x in after_wires}+ bad = set(now) != set(before) or any(abs(now[k] - before[k]) > 1e-6 and (k not in tkeys or abs(now[k] - w) > 1e-6) for k in now)+ if bad:+ _undo_until(app, args, meta['revision'], n_cmd + 2)+ fail('readback_mismatch', 'The copper after the width change was not exactly the asked wires at %.4f mm; every change was undone' % w)+ left = [x for x in left if abs(now[_wire_key(x)] - w) > 1e-6]+ # a stub lying wholly under a wider wire of its own net, or inside its own pad, adds no copper+ # at its width: it is covered, not a failure (Fusion leaves such stubs where a trace was redrawn)+ def under(x):+ def inside(p):+ return any(o['net'] == x['net'] and o['layer'] == x['layer'] and o['width'] >= w - 1e-6 and _seg_dist(p, o['start'], o['end']) <= (o['width'] - x['width']) / 2 + 1e-4+ for o in after_wires if _wire_key(o) != _wire_key(x)) or \+ any(q['net'] == x['net'] and x['layer'] in q['layers'] and math.hypot(p[0] - q['at'][0], p[1] - q['at'][1]) <= q['radius'] for q in pads.values())+ mid = ((x['start'][0] + x['end'][0]) / 2, (x['start'][1] + x['end'][1]) / 2)+ return all(inside(p) for p in (x['start'], mid, x['end']))+ covered = [x for x in left if under(x)]+ left = [x for x in left if x not in covered]+ app.executeTextCommand('Electron.run RATSNEST;')+ by = {}+ for x in targets:+ if x not in left and x not in covered: by[x['net']] = by.get(x['net'], 0) + 1+ out = {'success': not left, 'mutated': len(left) + len(covered) < len(targets), **meta2, 'changed': sum(by.values()), 'byNet': by, 'notOnBoard': missing, 'widthMm': w,+ 'covered': [{'net': x['net'], 'layer': x['layer'], 'start': x['start'], 'end': x['end'], 'widthMm': x['width']} for x in covered],+ '_hint': 'Read back: exactly these wires changed width, nothing else. Run fusion_run_drc to check clearances at the new width.'}+ if left:+ out['errorCode'] = 'partial'+ out['unchanged'] = [{'net': x['net'], 'layer': x['layer'], 'start': x['start'], 'end': x['end']} for x in left]+ out['_hint'] = ('%d wire(s) could not be picked on their own (a stub lying on another wire, or inside a pad): they keep their width; '+ 'the rest changed and were read back. Delete a redundant stub with fusion_remove_items if it is one.' % len(left))+ return out+++def remove_items(app, root, board, meta, args):+ """Delete tracks and vias given exactly as fusion_routing_state returned them; read back that+ exactly those went (each removal is its own undo step; a wrong one undoes all)."""+ if args.get('expectedRevision') not in (None, meta['revision']): fail('stale_board', 'expectedRevision must match the current live board')+ _, _, wires, vias = objects(board)+ L = Layers(board)+ num = {nm: n for n, nm in L.names.items()}+ results, gone_w, gone_v = [], [], []+ def xy(p): return (float(p['x']), float(p['y'])) if isinstance(p, dict) else (float(p[0]), float(p[1]))+ for t in args.get('tracks') or []:+ a, b = xy(t['start']), xy(t['end'])+ ln = num.get(t.get('layer'), t.get('layer'))+ hit = next((x for x in wires if x not in gone_w and (str(x['layer']) == str(ln)) and+ {tuple(map(lambda v: round(v, 4), x['start'])), tuple(map(lambda v: round(v, 4), x['end']))} == {tuple(map(lambda v: round(v, 4), a)), tuple(map(lambda v: round(v, 4), b))}), None)+ if hit: gone_w.append(hit); results.append('ok')+ else: results.append('err:not found')+ for v in args.get('vias') or []:+ p = xy(v.get('position', v))+ hit = next((x for x in vias if x not in gone_v and abs(x['at'][0] - p[0]) < 1e-4 and abs(x['at'][1] - p[1]) < 1e-4), None)+ if hit: gone_v.append(hit); results.append('ok')+ else: results.append('err:not found')+ if not (gone_w or gone_v):+ return {'success': False, 'errorCode': 'nothing_to_do', 'error': 'None of the given tracks or vias is on the board', 'results': results, **meta}+ visible = _visible_layers(root); n_cmd = 0+ try:+ for x in gone_w:+ app.executeTextCommand('Electron.run DISPLAY NONE %d;' % x['layer'])+ app.executeTextCommand('Electron.run DELETE (%.6fmm %.6fmm);' % ((x['start'][0] + x['end'][0]) / 2, (x['start'][1] + x['end'][1]) / 2)); n_cmd += 1+ for x in gone_v:+ app.executeTextCommand('Electron.run DISPLAY NONE 18;')+ app.executeTextCommand('Electron.run DELETE (%.6fmm %.6fmm);' % tuple(x['at'])); n_cmd += 1+ finally:+ _restore_display(app, visible)+ app.executeTextCommand('Electron.run RATSNEST;')+ _, after, meta2 = snapshot(app, args)+ _, _, w2, v2 = objects(after)+ expected = [x for x in wires if x not in gone_w] + [x for x in vias if x not in gone_v]+ if copper_signature(w2 + v2) != copper_signature(expected):+ _undo_until(app, args, meta['revision'], n_cmd + 2)+ fail('readback_mismatch', 'The board after the removal was not exactly the board minus the given items; every removal was undone')+ st = state(after, meta2)+ return {'success': all(r_ == 'ok' for r_ in results), 'mutated': True, **meta2, 'results': results, 'removed': {'tracks': len(gone_w), 'vias': len(gone_v)},+ 'unconnected': st['unconnected']}+++def set_outline(app, root, board, meta, args):+ """Replace the board outline (layer 20 Dimension) with a rectangle (rounded corners optional) or+ a polygon. The old board-level outline lines and arcs are deleted (each picked with only layer+ 20 displayed), the new one drawn, and the result read back as outlineEdges."""+ if args.get('expectedRevision') not in (None, meta['revision']): fail('stale_board', 'expectedRevision must match the current live board')+ o = args.get('outline') or {}+ if 'rect' in o:+ x0, y0, w, h = [number(v) for v in o['rect']]+ if w <= 0 or h <= 0: fail('bad_size', 'rect width and height must be > 0')+ r = number(o.get('cornerRadius', 0))+ if r * 2 > min(w, h): fail('bad_size', 'cornerRadius is more than half the shorter side')+ x1, y1 = x0 + w, y0 + h+ if r > 0:+ segs = [((x0 + r, y0), (x1 - r, y0), 0), ((x1 - r, y0), (x1, y0 + r), 90), ((x1, y0 + r), (x1, y1 - r), 0), ((x1, y1 - r), (x1 - r, y1), 90),+ ((x1 - r, y1), (x0 + r, y1), 0), ((x0 + r, y1), (x0, y1 - r), 90), ((x0, y1 - r), (x0, y0 + r), 0), ((x0, y0 + r), (x0 + r, y0), 90)]+ else:+ segs = [((x0, y0), (x1, y0), 0), ((x1, y0), (x1, y1), 0), ((x1, y1), (x0, y1), 0), ((x0, y1), (x0, y0), 0)]+ elif 'points' in o:+ pts = [(number(a), number(b)) for a, b in o['points']]+ if len(pts) < 3: fail('bad_args', 'A polygon outline needs at least three points')+ segs = [(pts[i], pts[(i + 1) % len(pts)], 0) for i in range(len(pts))]+ else:+ fail('bad_args', 'outline is {rect:[x0,y0,w,h], cornerRadius?} or {points:[[x,y]...]} (mm)')+ old = board.findall("plain/wire[@layer='20']")+ visible = _visible_layers(root); n_cmd = 0+ try:+ app.executeTextCommand('Electron.run DISPLAY NONE 20;')+ for wv in old:+ p1, p2, c = (_f(wv, 'x1'), _f(wv, 'y1')), (_f(wv, 'x2'), _f(wv, 'y2')), _f(wv, 'curve')+ pts_ = wire_points(p1, p2, c)+ mid = pts_[len(pts_) // 2] if c else ((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)+ app.executeTextCommand('Electron.run DELETE (%.6fmm %.6fmm);' % tuple(mid)); n_cmd += 1+ for a, b, curve in segs:+ # an arc is its signed angle bare (+90): '$90' and '@radius' are ignored without an error+ app.executeTextCommand('Electron.run LAYER 20; WIRE 0mm (%.6fmm %.6fmm) %s(%.6fmm %.6fmm);' % (a[0], a[1], ('%+g ' % curve) if curve else '', b[0], b[1])); n_cmd += 1+ finally:+ _restore_display(app, visible)+ app.executeTextCommand('Electron.run RATSNEST;')+ app.executeTextCommand('Electron.ImmediateRepaint /full')+ _, after, meta2 = snapshot(app, args)+ model, _ = board_model(after, meta2)+ drawn = after.findall("plain/wire[@layer='20']")+ ok = model['outlineClosed'] and len(drawn) == len(segs) and len(model['outlineEdges']) == len(segs)+ _, _, w0, v0 = objects(board); _, _, w1, v1 = objects(after)+ if copper_signature(w0 + v0) != copper_signature(w1 + v1) or not ok:+ _undo_until(app, args, meta['revision'], n_cmd + 2)+ fail('readback_mismatch', ('Copper changed while the outline was redrawn' if ok else+ 'The outline read back was not the one asked (%d of %d edges, closed: %s)' % (len(drawn), len(segs), model['outlineClosed'])) + '; every command was undone')+ return {'success': True, 'mutated': True, **meta2, 'outlineEdges': model['outlineEdges'], 'vertices': len(model['outline']),+ 'replaced': len(old), 'errorCode': None,+ '_hint': 'The outline is Fusion\'s Dimension layer (20), which its CAM output uses as the board edge. Check that parts and copper still sit inside it (fusion_routing_state outline, fusion_run_drc).'}+++def board_outline_read(app, root, board, meta, args):+ """Fusion's board edge for the fab is its Dimension layer (20): there is no separate mechanical+ outline to draw, so this reports what is there."""+ model, _ = board_model(board, meta)+ n = len(board.findall("plain/wire[@layer='20']")) + len(board.findall("plain/circle[@layer='20']"))+ return {'success': bool(n), 'mutated': False, **meta, 'layer': 'Dimension (20)', 'alreadyThere': n, 'drawn': 0, 'outlineClosed': model['outlineClosed'],+ 'errorCode': None if n else 'no_outline',+ '_hint': 'Fusion exports layer 20 (Dimension) as the board outline in its Gerbers, so nothing needs drawing. Change the shape with fusion_set_outline.' if n else 'The board has no outline: draw one with fusion_set_outline {outline:{rect:[x0,y0,w,h]}}.'}+++def _schematic_of(app, board_doc):+ """The schematic document of the same design as the board: a saved design's documents share+ their cloud file name and folder; an unsaved one's are its same-named tabs opened together+ (the schematic comes right before the board)."""+ docs = [app.documents.item(i) for i in range(app.documents.count)]+ def pt(d):+ try: return d.products.item(0).productType if d.products.count else ''+ except Exception: return ''+ if board_doc.dataFile is not None:+ ref = board_doc.dataFile+ for i, d in enumerate(docs):+ if pt(d) == 'SchematicProductType' and d.dataFile is not None and d.dataFile.name == ref.name and d.dataFile.parentFolder.id == ref.parentFolder.id:+ return i, d+ return None, None+ k = next(i for i, d in enumerate(docs) if d == board_doc)+ for i in range(k - 1, -1, -1):+ d = docs[i]+ if d.name != board_doc.name or d.dataFile is not None or pt(d) == 'EcadDesignProductType': break+ if pt(d) == 'SchematicProductType': return i, d+ return None, None+++def run_erc(app, root, board, meta, args):+ """Fusion's own ERC on the board's schematic (the design's schematic tab is brought forward for+ the check and the board's tab put back), every message read from UL_SCHEMATIC.errors."""+ bdoc = app.activeDocument+ i, sdoc = _schematic_of(app, bdoc)+ if sdoc is None:+ fail('no_schematic', 'This board has no schematic open beside it: open the design (its project document), not the board alone')+ j = next(k for k in range(app.documents.count) if app.documents.item(k) == bdoc)+ with tempfile.TemporaryDirectory(prefix='adom-erc-') as d:+ out = Path(d) / 'erc.tsv'; ulp = Path(d) / 'erc.ulp'+ ulp.write_text('schematic(S) { output("' + out.as_posix() + '") { printf("CHECKED\\n"); S.errors(E) { printf("ERROR\\t%d\\t%d\\t%d\\t%d\\t%d\\t%d\\t%s\\n", E.code, E.type, E.state, E.sheet, E.x, E.y, E.description); } } }')+ try:+ app.executeTextCommand('UI.SwitchToTab %d' % i)+ if app.activeDocument != sdoc: fail('wrong_document', 'Fusion did not bring the schematic tab forward for the ERC')+ app.executeTextCommand('Electron.run ERC;')+ app.executeTextCommand("Electron.run RUN '" + ulp.as_posix() + "';")+ finally:+ app.executeTextCommand('UI.SwitchToTab %d' % j)+ lines = out.read_text().splitlines() if out.exists() else []+ if not lines or lines[0] != 'CHECKED': fail('erc_failed', 'Fusion ran the ERC but its result could not be read')+ msgs = []+ for line in lines[1:]:+ _, code, kind, state_, sheet, x, y, text = line.split('\t', 7)+ cls = 'warning' if kind == '1' else 'error'+ m = {'class': cls, 'source': 'ERC', 'document': sdoc.name, 'text': text, 'code': int(code), 'sheet': int(sheet), 'approved': state_ == '2'}+ if int(sheet): m.update(x=round(int(x) / 320000.0, 4), y=round(int(y) / 320000.0, 4), unit='mm')+ msgs.append(m)+ by = {}+ for m in msgs: by[m['class']] = by.get(m['class'], 0) + 1+ kinds = {}+ for m in msgs: kinds[m['code']] = kinds.get(m['code'], 0) + 1+ return {'success': True, 'mutated': False, **meta, 'messages': msgs, 'messageCount': len(msgs), 'byClass': by,+ 'errorCount': sum(m['class'] == 'error' and not m['approved'] for m in msgs), 'warningCount': sum(m['class'] == 'warning' and not m['approved'] for m in msgs),+ 'byCode': kinds, 'schematic': sdoc.name,+ '_hint': 'Fusion\'s ERC messages, each with its code, sheet and position (mm, on that sheet). Code 104 "POWER pin connected to" is informational on a supply net; 105 "No pins on net" and 109 "Close but unconnected wires" are the ones that usually mean a wiring mistake. Fusion keeps the result in the schematic, so it reads as modified.'}+++def _copper_in_use(board):+ used = set()+ for w in board.findall('signals/signal/wire') + board.findall('signals/signal/polygon') + board.findall('plain/wire') + board.findall('plain/polygon'):+ n = int(w.get('layer', '0'))+ if n in COPPER: used.add(n)+ return used+++def autoroute(app, root, board, meta, args):+ """Fusion's autorouter (EAGLE AUTO) on the given nets, or every net with unconnected copper.+ Refused before any command when a copper layer in use is switched off in the board's autorouter+ setup: Fusion then asks a Yes/No question (layers "used but not enabled") that neither SET+ CONFIRM nor AUTO LOAD answers, so the bridge never runs into it. Existing copper is kept+ (read back: every track and via from before is still there, or the run is undone)."""+ if args.get('expectedRevision') not in (None, meta['revision']): fail('stale_board', 'expectedRevision must match the current live board')+ if args.get('lockPreroutes') is False:+ fail('unsupported', "Fusion's autorouter keeps existing tracks; to reroute a net, rip it up first (fusion_ripup_nets) and autoroute it")+ pref = {int(p.get('name').split('.')[1]): p.get('value') for p in board.findall("autorouter/pass[@name='Default']/param") if p.get('name', '').startswith('PrefDir.')}+ L = Layers(board)+ off = sorted(n for n in _copper_in_use(board) if pref.get(n, 'a') == '0')+ if off:+ fail('layers_not_enabled', "The board's autorouter setup has %s switched off while they carry copper, and Fusion would stop to ask whether to run anyway (a dialog this bridge never lets it draw). "+ "Turn them on once in Fusion's Autorouter dialog (ROUTE > Autorouter, layer directions; Fusion keeps the setting in the board), or route these nets with fusion_route_net." % ', '.join('%s (%d)' % (L.name(n), n) for n in off))+ st = state(board, meta)+ names = {n.lower(): n for n in st['nets']}+ if args.get('nets'):+ bad = [n for n in args['nets'] if n.lower() not in names]+ if bad: fail('no_such_net', 'No net named %s' % ', '.join(bad))+ nets = [names[n.lower()] for n in args['nets']]+ else:+ nets = [x['net'] for x in st.get('splitNets') or []]+ before = {'unconnected': st['unconnected']}+ if args.get('drc', True):+ before['errors'] = native_drc(app)['errorCount']+ if not nets:+ return {'success': True, 'mutated': False, **meta, 'nets': [], 'before': before, 'after': before, '_hint': 'Every net is already connected: nothing to route.'}+ _, _, w0, v0 = objects(board)+ app.executeTextCommand('Electron.run AUTO %s;' % ' '.join("'%s'" % identifier(n) for n in nets))+ _, after, meta2 = snapshot(app, args)+ _, _, w1, v1 = objects(after)+ k1 = {_wire_key(x) for x in w1}; p1 = {tuple(round(c, 4) for c in v['at']) for v in v1}+ if not all(_wire_key(x) in k1 for x in w0) or not all(tuple(round(c, 4) for c in v['at']) in p1 for v in v0):+ _undo_until(app, args, meta['revision'], 3)+ fail('readback_mismatch', 'The autorouter changed copper that was there before it ran; the run was undone')+ st2 = state(after, meta2)+ aft = {'unconnected': st2['unconnected']}+ if args.get('drc', True):+ aft['errors'] = native_drc(app)['errorCount']+ done = [n for n in nets if n not in {x['net'] for x in st2.get('splitNets') or []}]+ return {'success': True, 'mutated': len(w1) != len(w0) or len(v1) != len(v0), **meta2, 'nets': nets, 'routed': done,+ 'unrouted': [n for n in nets if n not in done], 'added': {'tracks': len(w1) - len(w0), 'vias': len(v1) - len(v0)}, 'before': before, 'after': aft,+ '_hint': "Fusion's autorouter ran on the listed nets; existing copper was kept. unrouted are nets it could not finish: route them with fusion_route_net or rip up and retry. Check after.errors with fusion_run_drc."}++ def handle(app,args,action): attempted=False; commands=[] try:@@ -943,6 +1329,17 @@ def handle(app,args,action): '_hint':'Columns: %s, then MPN, Manufacturer, LCSC, Mouser, DigiKey from each part\'s attributes. %d of %d parts carry an MPN and %d an LCSC number; data.missingMpn / missingLcsc name the rest (set them in the library or the part\'s attributes).'%('Comment, Designator, Footprint, Quantity, Library' if info['grouped'] else 'Designator, Comment, Footprint, Library, Side',info['withMpn'],n,info['withLcsc'])} if action=='apply_silkscreen': return apply_silkscreen(app,root,board,meta,args)+ if action=='autoroute': return autoroute(app,root,board,meta,args)+ if action=='run_erc': return run_erc(app,root,board,meta,args)+ if action=='set_layer_visibility': return set_layer_visibility(app,root,board,meta,args)+ if action=='select_net': return select_net(app,root,board,meta,args)+ if action=='set_track_width': return set_track_width(app,root,board,meta,args)+ if action=='remove_items': return remove_items(app,root,board,meta,args)+ if action=='board_outline': return board_outline_read(app,root,board,meta,args)+ if action in ('set_outline','board_shape'):+ if action=='board_shape':+ args=dict(args, outline={'rect':[args.get('x0Mm',0),args.get('y0Mm',0),args.get('wMm',15),args.get('hMm',15)]})+ return set_outline(app,root,board,meta,args) if action=='run_drc': # the native check on the live board, no revision guard: it changes no copper, parts or # outline (the revision), though Fusion keeps the DRC result in the board, which marks the
rust/bridge/src/services.rs+8−4@@ -85,12 +85,14 @@ pub(crate) fn clean_silk_png(bytes: &[u8]) -> Option<Vec<u8>> { Some(png_out) } -fn glb_submit(step_path: &str, silk_top: Option<&str>, silk_bottom: Option<&str>, pin: &str, job_name: &str) -> Value {+fn glb_submit(step_path: &str, silk_top: Option<&str>, silk_bottom: Option<&str>, pin: &str, compress: Option<&str>, job_name: &str) -> Value { let Ok(step) = std::fs::read(step_path) else { return json!({"ok": false, "error": format!("could not read STEP: {step_path}")}) }; let mut files: Vec<(&str, String, Vec<u8>, &str)> = vec![("step", std::path::Path::new(step_path).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default(), step, "application/step")]; for (field, p) in [("silk_top", silk_top), ("silk_bottom", silk_bottom)] { if let Some(p) = p { if let Ok(raw) = std::fs::read(p) { let b = clean_silk_png(&raw).unwrap_or(raw); files.push((field, std::path::Path::new(p).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default(), b, "image/png")); } } } let (body, boundary) = multipart(&files);- let url = format!("{}/convert?molecule=true&pin={}", step2glb_url(), fusion_core::aps::urlencode(pin));+ let mut url = format!("{}/convert?molecule=true&pin={}", step2glb_url(), fusion_core::aps::urlencode(pin));+ // the service's override (quantize | draco | none); omitted, a molecule is Draco-compressed+ if let Some(c) = compress { url.push_str(&format!("&compress={}", fusion_core::aps::urlencode(c))); } let resp = ureq::post(&url).timeout(Duration::from_secs(90)).set("Content-Type", &format!("multipart/form-data; boundary={boundary}")).set("X-Client", "fusion-bridge/adom").set("X-Job-Name", job_name).set("User-Agent", &service_ua()).send_bytes(&body); let queued: Value = match resp { Ok(r) => r.into_json().unwrap_or(json!({})), Err(e) => return json!({"ok": false, "error": format!("service POST failed: {e}"), "_hint": format!("Is {} reachable from this box? Override with ADOM_STEP2GLB_URL.", step2glb_url())}) }; match queued["job_id"].as_str() { Some(j) => json!({"ok": true, "jobId": j}), None => json!({"ok": false, "error": "service did not return a job_id", "raw": queued}) }@@ -122,6 +124,8 @@ pub fn export_optimized_glb(args: &Value, pass: &dyn Fn(&str, Value) -> Value) - if !out.to_lowercase().ends_with(".glb") { out.push_str(".glb"); } let pin = args["pin"].as_str().unwrap_or("medium").to_string(); let want_silk = args["silkscreen"].as_bool().unwrap_or(true);+ let compress = args["compress"].as_str().filter(|c| !c.is_empty()).map(str::to_lowercase);+ if let Some(c) = &compress { if !matches!(c.as_str(), "draco" | "quantize" | "none") { return json!({"success": false, "errorCode": "bad_args", "error": format!("compress is draco, quantize or none, not {c}"), "_hint": "compress:\"none\" gives a plain GLB that wiki README and widget frames can load (they cannot reach a Draco decoder)."}); } } let _ = pass("show_3d_board", json!({})); let step_path = format!("{}.step", &out[..out.len() - 4]); let sr = pass("export_step", json!({"outputPath": step_path}));@@ -134,7 +138,7 @@ pub fn export_optimized_glb(args: &Value, pass: &dyn Fn(&str, Value) -> Value) - if silk_top.is_none() && silk_bottom.is_none() { silk_note = Some("silkscreen capture unavailable on this design; GLB built without a baked silk texture."); } } let job_name = std::path::Path::new(&out).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();- let sub = glb_submit(&step_path, silk_top.as_deref(), silk_bottom.as_deref(), &pin, &job_name);+ let sub = glb_submit(&step_path, silk_top.as_deref(), silk_bottom.as_deref(), &pin, compress.as_deref(), &job_name); if sub["ok"].as_bool() != Some(true) { return json!({"success": false, "error": sub["error"], "_hint": sub["_hint"], "stepPath": step_path, "note": "STEP exported OK; optimize submit failed."}); } let job_id = sub["jobId"].as_str().unwrap_or("").to_string(); let wait_s = args["wait"].as_u64().unwrap_or(15);@@ -142,7 +146,7 @@ pub fn export_optimized_glb(args: &Value, pass: &dyn Fn(&str, Value) -> Value) - let (status_url, result_url) = (format!("{}/jobs/{job_id}", step2glb_url()), format!("{}/jobs/{job_id}/result", step2glb_url())); if fetched["status"].as_str() == Some("complete") && !fetched["wrote"].is_null() { let stats = &fetched["stats"]; let size = fetched["glbBytes"].as_u64().unwrap_or(0);- return json!({"success": true, "status": "complete", "glbPath": out, "stepPath": step_path, "jobId": job_id, "silkTop": silk_top, "silkBottom": silk_bottom, "meshesBefore": stats["meshes_before"], "meshesAfter": stats["meshes_after"], "sizeBytes": size, "moleculeAnchored": stats["molecule_anchored"], "silkscreenApplied": stats["silkscreen_applied"], "note": silk_note,+ return json!({"success": true, "status": "complete", "glbPath": out, "stepPath": step_path, "jobId": job_id, "silkTop": silk_top, "silkBottom": silk_bottom, "meshesBefore": stats["meshes_before"], "meshesAfter": stats["meshes_after"], "sizeBytes": size, "moleculeAnchored": stats["molecule_anchored"], "silkscreenApplied": stats["silkscreen_applied"], "compress": compress.as_deref().unwrap_or("draco (the molecule default)"), "note": silk_note, "message": format!("Optimized GLB written ({} KB, meshes {}->{}, anchored={}, silk={}) to {out}", size / 1024, crate::fusion_prefs::py_str(&stats["meshes_before"]), crate::fusion_prefs::py_str(&stats["meshes_after"]), crate::fusion_prefs::py_str(&stats["molecule_anchored"]), crate::fusion_prefs::py_str(&stats["silkscreen_applied"])), "_hint": "Pull it with pull_file, then set it as component.parts.model_3d on a wiki component page. Same optimizer as the molecule GLBs (Colby's pipeline)."}); }
tests/reports/verb-test-2026-10-27_1130.jsonadded+1334@@ -0,0 +1,1334 @@+{+ "date": "2026-10-27_1130",+ "target": "DESKTOP-BAR6S0F",+ "tier": "daily",+ "pass": 94,+ "fail": 5,+ "sections": {+ "bridge-lifecycle": {+ "PASS": 15,+ "FAIL": 0,+ "MANUAL": 17,+ "SKIP": 0+ },+ "auth-tokens": {+ "PASS": 2,+ "FAIL": 1,+ "MANUAL": 5,+ "SKIP": 0+ },+ "gui-driving": {+ "PASS": 20,+ "FAIL": 0,+ "MANUAL": 24,+ "SKIP": 0+ },+ "aps": {+ "PASS": 5,+ "FAIL": 4,+ "MANUAL": 15,+ "SKIP": 0+ },+ "mcp-server": {+ "PASS": 1,+ "FAIL": 0,+ "MANUAL": 1,+ "SKIP": 0+ },+ "cad-3d-bom": {+ "PASS": 15,+ "FAIL": 0,+ "MANUAL": 9,+ "SKIP": 0+ },+ "python-addin": {+ "PASS": 6,+ "FAIL": 0,+ "MANUAL": 1,+ "SKIP": 0+ },+ "routing": {+ "PASS": 29,+ "FAIL": 0,+ "MANUAL": 22,+ "SKIP": 0+ },+ "exports": {+ "PASS": 1,+ "FAIL": 0,+ "MANUAL": 2,+ "SKIP": 0+ }+ },+ "results": [+ {+ "verb": "fusion_readiness",+ "status": "PASS",+ "why": "",+ "sec": 0.3,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_prewarm",+ "status": "PASS",+ "why": "",+ "sec": 0.2,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_account",+ "status": "PASS",+ "why": "",+ "sec": 3.2,+ "section": "auth-tokens"+ },+ {+ "verb": "fusion_claim_seat",+ "status": "MANUAL",+ "why": "moves the Autodesk seat between machines; suspends another box's Fusion",+ "section": "auth-tokens"+ },+ {+ "verb": "fusion_release_seat",+ "status": "MANUAL",+ "why": "closes Fusion on the box to free the seat",+ "section": "auth-tokens"+ },+ {+ "verb": "fusion_describe",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_get_app_state",+ "status": "PASS",+ "why": "",+ "sec": 0.2,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_search_cloud_files",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "aps"+ },+ {+ "verb": "fusion_walk_cloud_tree",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "aps"+ },+ {+ "verb": "fusion_aps_status",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "auth-tokens"+ },+ {+ "verb": "fusion_aps_get_browser",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "aps"+ },+ {+ "verb": "fusion_aps_search",+ "status": "FAIL",+ "why": "expected success, got error=Not signed in to APS. code=aps_not_signed_in; expected >0 results",+ "sec": 3.1,+ "section": "aps"+ },+ {+ "verb": "fusion_aps_recent",+ "status": "FAIL",+ "why": "expected success, got error=Not signed in to APS. code=aps_not_signed_in; expected >0 results",+ "sec": 0.0,+ "section": "aps"+ },+ {+ "verb": "fusion_aps_browse",+ "status": "FAIL",+ "why": "expected success, got error=Not signed in to APS. code=aps_not_signed_in; missing field 'hubs'",+ "sec": 0.0,+ "section": "aps"+ },+ {+ "verb": "fusion_aps_file_info",+ "status": "FAIL",+ "why": "expected success, got error=Not signed in to APS. code=aps_not_signed_in; missing field 'versionCount'",+ "sec": 0.0,+ "section": "aps"+ },+ {+ "verb": "fusion_aps_get",+ "status": "FAIL",+ "why": "expected success, got error= code=None",+ "sec": 0.0,+ "section": "auth-tokens"+ },+ {+ "verb": "fusion_uninstall",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_set_seat_takeover",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_aps_create_folder",+ "status": "MANUAL",+ "why": "needs projectId/parentFolderId of the test workspace",+ "section": "aps"+ },+ {+ "verb": "fusion_aps_upload",+ "status": "MANUAL",+ "why": "cloud write; exercised via build flows weekly",+ "section": "aps"+ },+ {+ "verb": "fusion_aps_set_client_id",+ "status": "MANUAL",+ "why": "would clobber org auth config",+ "section": "auth-tokens"+ },+ {+ "verb": "fusion_aps_set_browser",+ "status": "MANUAL",+ "why": "",+ "section": "aps"+ },+ {+ "verb": "fusion_aps_forget_browser",+ "status": "MANUAL",+ "why": "",+ "section": "aps"+ },+ {+ "verb": "fusion_aps_signin",+ "status": "MANUAL",+ "why": "opens a browser on the user's machine",+ "section": "auth-tokens"+ },+ {+ "verb": "fusion_start",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_get_preferences",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_set_preference",+ "status": "PASS",+ "why": "",+ "sec": 1.1,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_mcp_status",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "mcp-server"+ },+ {+ "verb": "fusion_mcp_enable",+ "status": "MANUAL",+ "why": "drives the prefs UI, takes foreground",+ "section": "mcp-server"+ },+ {+ "verb": "fusion_prefs_open",+ "status": "MANUAL",+ "why": "opens a dialog on the user's screen",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_prefs_close",+ "status": "MANUAL",+ "why": "",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_set_auto_update",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_assembly_bom",+ "status": "PASS",+ "why": "",+ "sec": 1.3,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_dismiss_blocking_dialogs",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_open_cloud_file",+ "status": "PASS",+ "why": "",+ "sec": 20.2,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_show_schematic",+ "status": "PASS",+ "why": "",+ "sec": 3.5,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_show_2d_board",+ "status": "PASS",+ "why": "",+ "sec": 4.6,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_board_info",+ "status": "PASS",+ "why": "",+ "sec": 0.5,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_open_electronics",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "python-addin"+ },+ {+ "verb": "fusion_routing_state",+ "status": "PASS",+ "why": "",+ "sec": 1.9,+ "section": "routing"+ },+ {+ "verb": "fusion_layer_stack",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_netlist",+ "status": "PASS",+ "why": "",+ "sec": 1.4,+ "section": "routing"+ },+ {+ "verb": "fusion_pour_state",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_component_data",+ "status": "PASS",+ "why": "",+ "sec": 1.6,+ "section": "routing"+ },+ {+ "verb": "fusion_component_pins",+ "status": "PASS",+ "why": "",+ "sec": 1.6,+ "section": "routing"+ },+ {+ "verb": "fusion_list_nets",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_net_connections",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_list_designators",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_layers",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_copper_layers",+ "status": "PASS",+ "why": "",+ "sec": 1.4,+ "section": "routing"+ },+ {+ "verb": "fusion_layers_in_use",+ "status": "PASS",+ "why": "",+ "sec": 1.4,+ "section": "routing"+ },+ {+ "verb": "fusion_read_rules",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_rules_by_kind",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_silk_state",+ "status": "PASS",+ "why": "",+ "sec": 1.4,+ "section": "routing"+ },+ {+ "verb": "fusion_net_classes",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_board_summary",+ "status": "PASS",+ "why": "",+ "sec": 0.4,+ "section": "routing"+ },+ {+ "verb": "fusion_zoom_to",+ "status": "PASS",+ "why": "",+ "sec": 1.6,+ "section": "routing"+ },+ {+ "verb": "fusion_select_net",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_set_layer_visibility#hide",+ "status": "PASS",+ "why": "",+ "sec": 1.7,+ "section": "routing"+ },+ {+ "verb": "fusion_set_layer_visibility#show",+ "status": "PASS",+ "why": "",+ "sec": 1.8,+ "section": "routing"+ },+ {+ "verb": "fusion_board_outline",+ "status": "PASS",+ "why": "",+ "sec": 1.5,+ "section": "routing"+ },+ {+ "verb": "fusion_run_erc",+ "status": "PASS",+ "why": "",+ "sec": 4.8,+ "section": "routing"+ },+ {+ "verb": "fusion_status",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_check_dialogs",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_dialog_log",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_signin_status",+ "status": "PASS",+ "why": "",+ "sec": 2.9,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_license_status",+ "status": "PASS",+ "why": "",+ "sec": 2.9,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_list_commands",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_save",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias: the same verb as its target, which carries the tests",+ "section": "aps"+ },+ {+ "verb": "fusion_run_script",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias: the same verb as its target, which carries the tests",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_run_command",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias: the same verb as its target, which carries the tests",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_open_document",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias: the same verb as its target, which carries the tests",+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_show_3d_chip",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias: the same verb as its target, which carries the tests",+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_export_bom#parts",+ "status": "PASS",+ "why": "",+ "sec": 2.5,+ "section": "exports"+ },+ {+ "verb": "fusion_list_text_commands",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "python-addin"+ },+ {+ "verb": "fusion_execute_text_command",+ "status": "PASS",+ "why": "",+ "sec": 1.9,+ "section": "python-addin"+ },+ {+ "verb": "fusion_bodies",+ "status": "PASS",+ "why": "",+ "sec": 14.3,+ "section": "routing"+ },+ {+ "verb": "fusion_show_3d_board",+ "status": "PASS",+ "why": "",+ "sec": 4.9,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_inspect_bodies",+ "status": "PASS",+ "why": "",+ "sec": 2.3,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_physical_properties",+ "status": "PASS",+ "why": "",+ "sec": 0.5,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_get_design_info",+ "status": "PASS",+ "why": "",+ "sec": 0.7,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_get_parameters",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_document_info",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_delete_cloud_file#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.9,+ "section": "aps"+ },+ {+ "verb": "fusion_delete_cloud_file",+ "status": "MANUAL",+ "why": "destructive cloud delete; only against files a rehearsal created",+ "section": "aps"+ },+ {+ "verb": "fusion_create_cloud_folder",+ "status": "MANUAL",+ "why": "cloud write into the team hub",+ "section": "aps"+ },+ {+ "verb": "fusion_set_parameter#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.9,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_set_parameter",+ "status": "MANUAL",+ "why": "mutates the active design's parameters",+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_run_modeling_script#guard",+ "status": "PASS",+ "why": "",+ "sec": 2.3,+ "section": "python-addin"+ },+ {+ "verb": "fusion_assembly_bom#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.3,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_run_modeling_script",+ "status": "PASS",+ "why": "",+ "sec": 1.7,+ "section": "python-addin"+ },+ {+ "verb": "fusion_electron_select",+ "status": "MANUAL",+ "why": "selection state is user-visible",+ "section": "python-addin"+ },+ {+ "verb": "fusion_open_design",+ "status": "MANUAL",+ "why": "local-file open; superseded by cloud flows",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_open_board",+ "status": "MANUAL",+ "why": "local .brd open; needs a .brd on the box (make_test_project exercises the same newDesignFromLocal path)",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_open_schematic",+ "status": "MANUAL",+ "why": "local .sch open; needs a .sch on the box (make_test_project exercises the same newDesignFromLocal path)",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_import_electronics",+ "status": "MANUAL",+ "why": "needs a Fusion-native .fsch/.fbrd/.flbr on the box; creates a NEW unsaved document",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_close_window#noargs",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_close_document",+ "status": "MANUAL",+ "why": "",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_close_all_documents",+ "status": "MANUAL",+ "why": "would close the user's own tabs",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_export_obj",+ "status": "MANUAL",+ "why": "build/doc-type dependent (OBJ refused on the 2026-08-14 box); verify quarterly",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_export_fbx",+ "status": "MANUAL",+ "why": "build/doc-type dependent (OBJ refused on the 2026-08-14 box); verify quarterly",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_export_usdz",+ "status": "MANUAL",+ "why": "build/doc-type dependent (OBJ refused on the 2026-08-14 box); verify quarterly",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_export_dxf",+ "status": "MANUAL",+ "why": "build/doc-type dependent (OBJ refused on the 2026-08-14 box); verify quarterly",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_export_dwg",+ "status": "MANUAL",+ "why": "build/doc-type dependent (OBJ refused on the 2026-08-14 box); verify quarterly",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_export_skp",+ "status": "MANUAL",+ "why": "build/doc-type dependent (OBJ refused on the 2026-08-14 box); verify quarterly",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_set_design_rules",+ "status": "MANUAL",+ "why": "mutates the board's DRC rules",+ "section": "exports"+ },+ {+ "verb": "fusion_show_3d_package",+ "status": "MANUAL",+ "why": "enters Package3DEnvironment (exit needs Package3DStop + dialog); tour covers it; attach path in release rehearsals",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_bind_library_3d",+ "status": "MANUAL",+ "why": "cloud f3d mint per run; release rehearsals (needs signed-in session)",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_attach_3d_package",+ "status": "MANUAL",+ "why": "modal Save dialog needs a human eye",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_make_3d_package",+ "status": "MANUAL",+ "why": "cloud upload; run in release rehearsals",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_build_library_3d",+ "status": "MANUAL",+ "why": "long multi-part build; release rehearsals",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_cleanup_cloud_files#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.9,+ "section": "aps"+ },+ {+ "verb": "fusion_cleanup_cloud_files",+ "status": "MANUAL",+ "why": "real cleanup takes explicit lineage urns; used by weekly build rehearsals",+ "section": "aps"+ },+ {+ "verb": "fusion_new_electronics_from_eagle",+ "status": "MANUAL",+ "why": "drives native dialogs",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_screenshot_fusion",+ "status": "PASS",+ "why": "",+ "sec": 0.6,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_state",+ "status": "PASS",+ "why": "",+ "sec": 0.6,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_demo_progress#unknown",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_stop",+ "status": "MANUAL",+ "why": "lifecycle/human-wall verbs; manual tier",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_kill",+ "status": "MANUAL",+ "why": "lifecycle/human-wall verbs; manual tier",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_signin",+ "status": "MANUAL",+ "why": "lifecycle/human-wall verbs; manual tier",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_signin_2fa",+ "status": "MANUAL",+ "why": "lifecycle/human-wall verbs; manual tier",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_demo",+ "status": "MANUAL",+ "why": "lifecycle/human-wall verbs; manual tier",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_install_fusion",+ "status": "MANUAL",+ "why": "lifecycle/human-wall verbs; manual tier",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_notify_owner",+ "status": "MANUAL",+ "why": "lifecycle/human-wall verbs; manual tier",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_open_by_urn_note",+ "status": "MANUAL",+ "why": "lifecycle/human-wall verbs; manual tier",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_action_ledger",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_addin_status",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "python-addin"+ },+ {+ "verb": "fusion_alerts",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_verb_times",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_live_feed",+ "status": "PASS",+ "why": "",+ "sec": 0.1,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_prove_result",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_set_driving_badge",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_set_window_sizing",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_cancel_run",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_preflight_lbr",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_library_parts",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_create_library",+ "status": "MANUAL",+ "why": "the five-part drill covers it (fusion_install_library_bundle -> prove)",+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_install_library_bundle",+ "status": "MANUAL",+ "why": "the five-part drill covers it; needs a bundle zip on the box",+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_managed_library_create",+ "status": "MANUAL",+ "why": "creates a managed library on the hub (cloud write)",+ "section": "aps"+ },+ {+ "verb": "fusion_managed_library_link",+ "status": "MANUAL",+ "why": "drives LIBRARY.IO dialogs against the hub",+ "section": "aps"+ },+ {+ "verb": "fusion_managed_library_pull",+ "status": "MANUAL",+ "why": "drives LIBRARY.IO dialogs against the hub",+ "section": "aps"+ },+ {+ "verb": "fusion_managed_library_push",+ "status": "MANUAL",+ "why": "drives LIBRARY.IO dialogs; cloud write",+ "section": "aps"+ },+ {+ "verb": "fusion_managed_library_unlink",+ "status": "MANUAL",+ "why": "drives LIBRARY.IO dialogs against the hub",+ "section": "aps"+ },+ {+ "verb": "fusion_reset_workspace",+ "status": "MANUAL",+ "why": "closes the user's documents",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_restart_for_addin",+ "status": "MANUAL",+ "why": "restarts Fusion",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_save_to_cloud",+ "status": "MANUAL",+ "why": "cloud write; exercised by the drills",+ "section": "aps"+ },+ {+ "verb": "fusion_web_signin",+ "status": "MANUAL",+ "why": "opens a browser on the user's machine",+ "section": "auth-tokens"+ },+ {+ "verb": "fusion_routing_state#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.0,+ "section": "routing"+ },+ {+ "verb": "fusion_route_net#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.0,+ "section": "routing"+ },+ {+ "verb": "fusion_routing_drc#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.0,+ "section": "routing"+ },+ {+ "verb": "fusion_plan_route#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.0,+ "section": "routing"+ },+ {+ "verb": "fusion_route_net",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_routing_drc",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_plan_route",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_move_parts",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_place_vias",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_apply_pours",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_board_source",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_ripup_nets",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_schematic_run",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_board_bom",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_change_pours",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_board_seed",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_schematic_plan",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_remove_pours",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_run_drc",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_repour",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_apply_silkscreen",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_set_track_width",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_remove_items",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_set_outline",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_board_shape",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_autoroute",+ "status": "MANUAL",+ "why": "live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)",+ "section": "routing"+ },+ {+ "verb": "fusion_apply_fusion_update",+ "status": "MANUAL",+ "why": "lifecycle, seat or presentation verbs: need a human-visible Fusion state; manual tier",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_resolve_seat_dialog",+ "status": "MANUAL",+ "why": "lifecycle, seat or presentation verbs: need a human-visible Fusion state; manual tier",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_interactive",+ "status": "MANUAL",+ "why": "lifecycle, seat or presentation verbs: need a human-visible Fusion state; manual tier",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_narrate",+ "status": "MANUAL",+ "why": "lifecycle, seat or presentation verbs: need a human-visible Fusion state; manual tier",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_view",+ "status": "MANUAL",+ "why": "lifecycle, seat or presentation verbs: need a human-visible Fusion state; manual tier",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_electronics_query",+ "status": "MANUAL",+ "why": "lifecycle, seat or presentation verbs: need a human-visible Fusion state; manual tier",+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_schematic#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.9,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_launch",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias of an existing verb (the reply carries aliasOf); tested through the verb it names",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_export_gerber",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias of an existing verb (the reply carries aliasOf); tested through the verb it names",+ "section": "exports"+ },+ {+ "verb": "fusion_dismiss_dialogs",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias of an existing verb (the reply carries aliasOf); tested through the verb it names",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_install",+ "status": "MANUAL",+ "why": "EDA Bridge Standard alias of an existing verb (the reply carries aliasOf); tested through the verb it names",+ "section": "bridge-lifecycle"+ },+ {+ "verb": "fusion_save_document",+ "status": "MANUAL",+ "why": "saves a cloud design in place (a new version): exercised live on a scratch design in a test folder",+ "section": "aps"+ },+ {+ "verb": "fusion_read_sheet_file#guard",+ "status": "PASS",+ "why": "",+ "sec": 0.0,+ "section": "routing"+ },+ {+ "verb": "fusion_schematic#missing",+ "status": "PASS",+ "why": "",+ "sec": 2.9,+ "section": "gui-driving"+ },+ {+ "verb": "fusion_schematic",+ "status": "MANUAL",+ "why": "open a cloud design's schematic / create a blank one: exercised by the weekly tour",+ "section": "gui-driving"+ },+ {+ "verb": "fusion_make_showcase_project#guard",+ "status": "PASS",+ "why": "",+ "sec": 2.5,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_make_showcase_project",+ "status": "MANUAL",+ "why": "places a library's parts on a fresh sch+brd; exercised on the Adom_ToF library",+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_eagle_import_status#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.9,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_eagle_import_status",+ "status": "MANUAL",+ "why": "status of a background EAGLE import",+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_show_attributes#guard",+ "status": "PASS",+ "why": "",+ "sec": 1.9,+ "section": "cad-3d-bom"+ },+ {+ "verb": "fusion_show_attributes",+ "status": "MANUAL",+ "why": "opens the device editor for a deviceset and returns its metadata",+ "section": "cad-3d-bom"+ },+ {+ "verb": "focus_invariance",+ "status": "PASS",+ "why": "",+ "section": "gui-driving"+ }+ ],+ "durationSec": 138.1+}
tests/run_verb_tests.py+14−2@@ -201,6 +201,18 @@ t("fusion_silk_state", "daily", B, {"ok": True, "fields": ["components", "texts" t("fusion_net_classes", "daily", B, {"ok": True, "fields": ["classes"]}) t("fusion_board_summary", "daily", {}, {"ok": True, "fields": ["aliasOf"]}, "EDA Bridge Standard alias of fusion_board_info") t("fusion_zoom_to", "daily", dict(B, ref="IC1"), {"ok": True, "fields": ["rect"]}, "frames the view; changes nothing")+t("fusion_select_net", "daily", dict(B, net="VBUS", zoom=True), {"ok": True, "fields": ["selected", "byKind", "zoom"]}, "highlights the net (SHOW left waiting for a click) and frames it; changes nothing")+t("fusion_set_layer_visibility#hide", "daily", dict(B, layerId="B.Cu", displayed=False), {"ok": True, "fields": ["displayed"]}, "read back from the board's layer flags")+t("fusion_set_layer_visibility#show", "daily", dict(B, layerId="B.Cu", displayed=True), {"ok": True, "fields": ["displayed"]})+t("fusion_board_outline", "daily", B, {"ok": True, "fields": ["alreadyThere", "outlineClosed"]}, "Fusion's edge is the Dimension layer: reports it, draws nothing")+t("fusion_run_erc", "daily", B, {"ok": True, "fields": ["messages", "messageCount", "byClass"]}, "brings the design's schematic tab forward for the ERC and puts the board back")+t("fusion_status", "daily", {}, {"ok": True, "fields": ["aliasOf"]}, "EDA Bridge Standard alias of fusion_readiness")+t("fusion_check_dialogs", "daily", {}, {"ok": True, "fields": ["dialogs", "count"]}, "UI Automation read; never touches the add-in")+for v, own in (("fusion_dialog_log", "fusion_action_ledger"), ("fusion_signin_status", "fusion_account"), ("fusion_license_status", "fusion_account")):+ t(v, "daily", {}, {"ok": True, "fields": ["aliasOf"]}, "EDA Bridge Standard alias of " + own)+t("fusion_list_commands", "daily", {"filter": "Electron"}, {"ok": True, "fields": ["aliasOf", "commands"]}, "alias of fusion_list_text_commands")+for v in ("fusion_save", "fusion_run_script", "fusion_run_command", "fusion_open_document", "fusion_show_3d_chip"):+ t(v, "manual", note="EDA Bridge Standard alias: the same verb as its target, which carries the tests") t("fusion_export_bom#parts", "daily", {"outputPath": f"{SCRATCH}/bom-parts.csv"}, {"ok": True, "pred": "bom_has_part_numbers"}, "1.12.0: MPN/LCSC columns from the part attributes (page issue #105)") t("fusion_list_text_commands", "daily", {"filter": "Electron"}, {"ok": True, "fields": ["commands"]}, "TextCommands.List needs the electronics editor context") t("fusion_export_source", "weekly", {"outputPath": f"{SCRATCH}/verbtest.fbrd"}, {"ok": True, "fields": ["savedTo"]}, ".fbrd needs the 2D board view ACTIVE (Document.CopyToDesktop)")@@ -370,7 +382,7 @@ t("fusion_route_net#guard", "negative", {"expectDocument": "ZZZ_NoSuchDoc_TEST", "board/revision guards must refuse before any copper is drawn") t("fusion_routing_drc#guard", "negative", {"expectDocument": "ZZZ_NoSuchDoc_TEST", "boardId": "x", "expectedRevision": "x"}, {"pred": "must_refuse"}, "same guards as route_net") t("fusion_plan_route#guard", "negative", {"expectDocument": "ZZZ_NoSuchDoc_TEST", "net": "NET_1"}, {"pred": "must_refuse"}, "planner needs the live board it was asked about")-for v in ["fusion_route_net", "fusion_routing_drc", "fusion_plan_route", "fusion_move_parts", "fusion_place_vias", "fusion_apply_pours", "fusion_board_source", "fusion_ripup_nets", "fusion_schematic_run", "fusion_board_bom", "fusion_change_pours", "fusion_board_seed", "fusion_schematic_plan", "fusion_remove_pours", "fusion_run_drc", "fusion_repour", "fusion_apply_silkscreen"]:+for v in ["fusion_route_net", "fusion_routing_drc", "fusion_plan_route", "fusion_move_parts", "fusion_place_vias", "fusion_apply_pours", "fusion_board_source", "fusion_ripup_nets", "fusion_schematic_run", "fusion_board_bom", "fusion_change_pours", "fusion_board_seed", "fusion_schematic_plan", "fusion_remove_pours", "fusion_run_drc", "fusion_repour", "fusion_apply_silkscreen", "fusion_set_track_width", "fusion_remove_items", "fusion_set_outline", "fusion_board_shape", "fusion_autoroute"]: t(v, "manual", note="live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)") for v in ["fusion_apply_fusion_update", "fusion_resolve_seat_dialog", "fusion_interactive", "fusion_narrate", "fusion_view", "fusion_electronics_query"]: t(v, "manual", note="lifecycle, seat or presentation verbs: need a human-visible Fusion state; manual tier")@@ -393,7 +405,7 @@ _SECTION_BY_VERB = { "fusion_verb_times": "bridge-lifecycle", "fusion_live_feed": "bridge-lifecycle", "fusion_account": "auth-tokens", "fusion_claim_seat": "auth-tokens", "fusion_release_seat": "auth-tokens", "fusion_prewarm": "bridge-lifecycle", "fusion_prove_result": "cad-3d-bom", "fusion_set_driving_badge": "bridge-lifecycle", "fusion_set_window_sizing": "gui-driving", "fusion_cancel_run": "cad-3d-bom", "fusion_preflight_lbr": "cad-3d-bom", "fusion_library_parts": "cad-3d-bom", "fusion_take_screenshot": "gui-driving",- "fusion_schematic": "gui-driving", "fusion_show_attributes": "cad-3d-bom", "fusion_make_showcase_project": "cad-3d-bom", "fusion_eagle_import_status": "cad-3d-bom", "fusion_routing_state": "routing", "fusion_route_net": "routing", "fusion_routing_drc": "routing", "fusion_plan_route": "routing", "fusion_move_parts": "routing", "fusion_place_vias": "routing", "fusion_apply_pours": "routing", "fusion_pour_state": "routing", "fusion_board_source": "routing", "fusion_ripup_nets": "routing", "fusion_schematic_run": "routing", "fusion_board_bom": "routing", "fusion_change_pours": "routing", "fusion_board_seed": "routing", "fusion_schematic_plan": "routing", "fusion_remove_pours": "routing", "fusion_layer_stack": "routing", "fusion_netlist": "routing", "fusion_read_sheet_file": "routing", "fusion_bodies": "routing", "fusion_save_document": "aps", "fusion_apply_silkscreen": "routing", "fusion_component_data": "routing", "fusion_component_pins": "routing", "fusion_list_nets": "routing", "fusion_net_connections": "routing", "fusion_list_designators": "routing", "fusion_layers": "routing", "fusion_copper_layers": "routing", "fusion_layers_in_use": "routing", "fusion_read_rules": "routing", "fusion_rules_by_kind": "routing", "fusion_silk_state": "routing", "fusion_net_classes": "routing", "fusion_run_drc": "routing", "fusion_repour": "routing", "fusion_zoom_to": "routing", "fusion_launch": "bridge-lifecycle", "fusion_export_gerber": "exports", "fusion_dismiss_dialogs": "gui-driving", "fusion_board_summary": "routing", "fusion_install": "bridge-lifecycle",+ "fusion_schematic": "gui-driving", "fusion_show_attributes": "cad-3d-bom", "fusion_make_showcase_project": "cad-3d-bom", "fusion_eagle_import_status": "cad-3d-bom", "fusion_routing_state": "routing", "fusion_route_net": "routing", "fusion_routing_drc": "routing", "fusion_plan_route": "routing", "fusion_move_parts": "routing", "fusion_place_vias": "routing", "fusion_apply_pours": "routing", "fusion_pour_state": "routing", "fusion_board_source": "routing", "fusion_ripup_nets": "routing", "fusion_schematic_run": "routing", "fusion_board_bom": "routing", "fusion_change_pours": "routing", "fusion_board_seed": "routing", "fusion_schematic_plan": "routing", "fusion_remove_pours": "routing", "fusion_layer_stack": "routing", "fusion_netlist": "routing", "fusion_read_sheet_file": "routing", "fusion_bodies": "routing", "fusion_save_document": "aps", "fusion_apply_silkscreen": "routing", "fusion_component_data": "routing", "fusion_component_pins": "routing", "fusion_list_nets": "routing", "fusion_net_connections": "routing", "fusion_list_designators": "routing", "fusion_layers": "routing", "fusion_copper_layers": "routing", "fusion_layers_in_use": "routing", "fusion_read_rules": "routing", "fusion_rules_by_kind": "routing", "fusion_silk_state": "routing", "fusion_net_classes": "routing", "fusion_run_drc": "routing", "fusion_repour": "routing", "fusion_zoom_to": "routing", "fusion_launch": "bridge-lifecycle", "fusion_export_gerber": "exports", "fusion_dismiss_dialogs": "gui-driving", "fusion_board_summary": "routing", "fusion_install": "bridge-lifecycle", "fusion_select_net": "routing", "fusion_set_layer_visibility": "routing", "fusion_board_outline": "routing", "fusion_run_erc": "routing", "fusion_set_track_width": "routing", "fusion_remove_items": "routing", "fusion_set_outline": "routing", "fusion_board_shape": "routing", "fusion_autoroute": "routing", "fusion_check_dialogs": "gui-driving", "fusion_status": "bridge-lifecycle", "fusion_save": "aps", "fusion_run_script": "gui-driving", "fusion_run_command": "gui-driving", "fusion_list_commands": "gui-driving", "fusion_dialog_log": "gui-driving", "fusion_signin_status": "bridge-lifecycle", "fusion_license_status": "bridge-lifecycle", "fusion_open_document": "cad-3d-bom", "fusion_show_3d_chip": "cad-3d-bom", "fusion_apply_fusion_update": "bridge-lifecycle", "fusion_resolve_seat_dialog": "bridge-lifecycle", "fusion_interactive": "gui-driving", "fusion_narrate": "gui-driving", "fusion_view": "gui-driving", "fusion_electronics_query": "cad-3d-bom", "fusion_activate_document": "gui-driving", "fusion_library_manager": "gui-driving", "fusion_plan_library_import": "cad-3d-bom", "fusion_create_library": "cad-3d-bom", "fusion_install_library_bundle": "cad-3d-bom",
tests/test_routing.py+86@@ -169,4 +169,90 @@ class NetNameTests(unittest.TestCase): r.identifier(n) +class EditVerbTests(unittest.TestCase):+ """The board-edit verbs against a recording app: the commands sent, the display restored one+ layer per command, and a read back that does not match undone."""+ def setUp(self):+ self.root = ET.parse(ROOT/'demo/routing/fixture/fusion-routing.brd').getroot()+ self.board = self.root.find('drawing/board')+ sig = self.board.find("signals/signal[@name='NET_1']")+ for x1, x2, layer in ((0, 10, 1), (10, 20, 1), (20, 30, 16)):+ ET.SubElement(sig, 'wire', x1=str(x1), y1='5', x2=str(x2), y2='5', width='0.2', layer=str(layer))+ ET.SubElement(sig, 'via', x='20', y='5', extent='1-16', drill='0.3', diameter='0.6')+ self.sent, self.after = [], self.board+ test = self+ class App:+ def executeTextCommand(self, cmd): test.sent.append(cmd)+ self.app = App()+ self.saved = r.snapshot+ r.snapshot = lambda app, args: (self.root, self.after, {'boardId': 'b', 'revision': 'r1' if self.after is self.board else 'r2'})+ def tearDown(self): r.snapshot = self.saved+ def runs(self): return [c[len('Electron.run '):] for c in self.sent if c.startswith('Electron.run ')]+ def edited(self, fn):+ b = copy.deepcopy(self.board); fn(b); self.after = b+ def test_layer_names(self):+ self.assertEqual([1, 16, 16, 20], [r._layer_number(self.board, v) for v in ('F.Cu', 'bottom', 'B.Cu', '20')])+ with self.assertRaises(r.RoutingError): r._layer_number(self.board, 'In9.Cu')+ def test_track_width_changes_only_the_asked_wires(self):+ def widen(b):+ for w in b.findall("signals/signal[@name='NET_1']/wire[@layer='1']"): w.set('width', '0.5')+ self.edited(widen)+ out = r.set_track_width(self.app, self.root, self.board, {'boardId': 'b', 'revision': 'r1'}, {'nets': ['NET_1', 'NOPE'], 'layer': 'F.Cu', 'widthMm': 0.5})+ self.assertTrue(out['success']); self.assertEqual(2, out['changed']); self.assertEqual(['NOPE'], out['notOnBoard'])+ runs = self.runs()+ picks = [tuple(map(float, __import__('re').findall(r'([\d.]+)mm', c)[1:])) for c in runs if c.startswith('CHANGE WIDTH')]+ # one pick per wire, on it, and away from the junction the two share at x=10+ self.assertEqual(2, len(picks)); self.assertTrue(0 < picks[0][0] < 10 < picks[1][0] < 20, picks)+ self.assertTrue(all(abs(p[0] - 10) > 4 and p[1] == 5 for p in picks), picks)+ self.assertNotIn('DISPLAY 23;', runs); self.assertIn('DISPLAY 1;', runs); self.assertIn('DISPLAY 21;', runs)+ def test_track_width_mismatch_is_undone(self):+ def wrong(b):+ for w in b.findall("signals/signal[@name='NET_1']/wire"): w.set('width', '0.5')+ self.edited(wrong)+ with self.assertRaises(r.RoutingError) as e:+ r.set_track_width(self.app, self.root, self.board, {'boardId': 'b', 'revision': 'r1'}, {'nets': ['NET_1'], 'layer': 1, 'widthMm': 0.5})+ self.assertEqual('readback_mismatch', e.exception.code); self.assertIn('UNDO;', self.runs())+ def test_remove_items(self):+ def drop(b):+ s = b.find("signals/signal[@name='NET_1']")+ s.remove(s.find("wire[@x1='20']")); s.remove(s.find('via'))+ self.edited(drop)+ out = r.remove_items(self.app, self.root, self.board, {'boardId': 'b', 'revision': 'r1'},+ {'tracks': [{'start': {'x': 30, 'y': 5}, 'end': {'x': 20, 'y': 5}, 'layer': 'B.Cu'}, {'start': [1, 1], 'end': [2, 2], 'layer': 'F.Cu'}],+ 'vias': [{'position': {'x': 20, 'y': 5}}]})+ self.assertEqual(['ok', 'err:not found', 'ok'], out['results']); self.assertEqual({'tracks': 1, 'vias': 1}, out['removed'])+ self.assertIn('DELETE (25.000000mm 5.000000mm);', self.runs())+ def test_stale_revision(self):+ with self.assertRaises(r.RoutingError) as e:+ r.remove_items(self.app, self.root, self.board, {'boardId': 'b', 'revision': 'r1'}, {'expectedRevision': 'old', 'vias': [[20, 5]]})+ self.assertEqual('stale_board', e.exception.code); self.assertEqual([], self.sent)+ def test_rounded_rect_outline(self):+ def redraw(b):+ for w in b.findall("plain/wire[@layer='20']"): b.find('plain').remove(w)+ for c in self.runs():+ m = __import__('re').match(r'LAYER 20; WIRE 0mm \(([-\d.]+)mm ([-\d.]+)mm\) (?:\+(\d+) )?\(([-\d.]+)mm ([-\d.]+)mm\);', c)+ if m: ET.SubElement(b.find('plain'), 'wire', x1=m[1], y1=m[2], x2=m[4], y2=m[5], width='0', layer='20', **({'curve': m[3]} if m[3] else {}))+ out_ = {}+ real = self.app.executeTextCommand+ def cmd(c):+ real(c)+ if c == 'Electron.run RATSNEST;': self.edited(redraw)+ self.app.executeTextCommand = cmd+ out = r.set_outline(self.app, self.root, self.board, {'boardId': 'b', 'revision': 'r1'}, {'outline': {'rect': [0, 0, 60, 40], 'cornerRadius': 2}})+ self.assertTrue(out['success'], out); self.assertEqual(8, len(out['outlineEdges'])); self.assertEqual(4, out['replaced'])+ self.assertEqual(4, sum(') +90 (' in c for c in self.runs()))+ for bad in ({'rect': [0, 0, 10, 10], 'cornerRadius': 6}, {'points': [[0, 0], [1, 1]]}, {}):+ with self.assertRaises(r.RoutingError): r.set_outline(self.app, self.root, self.board, {'boardId': 'b', 'revision': 'r1'}, {'outline': bad})++ def test_autoroute_refuses_disabled_layers_before_any_command(self):+ sig = self.board.find("signals/signal[@name='NET_2']")+ ET.SubElement(sig, 'wire', x1='1', y1='1', x2='2', y2='1', width='0.2', layer='2')+ ar = self.board.find('autorouter') or ET.SubElement(self.board, 'autorouter')+ ps = ET.SubElement(ar, 'pass', name='Default')+ for n in range(1, 17): ET.SubElement(ps, 'param', name='PrefDir.%d' % n, value='a' if n in (1, 16) else '0')+ with self.assertRaises(r.RoutingError) as e:+ r.autoroute(self.app, self.root, self.board, {'boardId': 'b', 'revision': 'r1'}, {})+ self.assertEqual('layers_not_enabled', e.exception.code); self.assertIn('(2)', str(e.exception)); self.assertEqual([], self.sent)++ if __name__=='__main__':unittest.main()