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
Update 166 file(s)
20 files changed
+102−65
BRIDGE_VERSION+1−1addin/AdomBridge/commands/manufacturing.py+7−7bridge.json+1−1contracts/describe.json+4−3dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1describe.py+2−2docs/board-layout-2d.md+3−3install.sh+1−1package.json+1−1page.json+10−26rust/Cargo.toml+1−1rust/bridge/src/orchestrations.rs+60−8rust/bridge/src/services.rs+2−2server.py+6−6skills/fusion-electronics/SKILL.md+1−1tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.shBRIDGE_VERSION+1−1@@ -1 +1 @@-1.11.9+1.11.10
addin/AdomBridge/commands/manufacturing.py+7−7@@ -5,7 +5,7 @@ Electronics workspace and write industry-standard output files: - Gerbers (RS-274X) + Excellon drill files via EAGLE CAM processor - BOM (Bill of Materials) CSV for component purchasing - CPL (Component Placement List) CSV for pick-and-place assembly- - Design rules (DRU) injection for the 3rd party fab's manufacturing capabilities+ - Design rules (DRU) injection for Adom's InstapcbPCB manufacturing capabilities All three export commands require a .brd board to be open in PCB Editor. @@ -769,11 +769,11 @@ def _parse_edru_description(edru_path: str) -> str: def handle_set_design_rules(app: adsk.core.Application, args: dict) -> dict:- """Set PCB design rules in the open board to match the 3rd party fab's manufacturing capabilities.+ """Set PCB design rules in the open board to match Adom's manufacturing capabilities. Loads a bundled .edru design rules file via EAGLE's `drc load` command.- The .edru files are industry-tested rules from JLCPCB, adapted for the 3rd party- fab's manufacturing process.+ The .edru files are industry-tested rules from JLCPCB, adapted for Adom's+ InstapcbPCB manufacturing process. Auto-detects 2-layer vs 4-layer boards and loads the appropriate rule set. @@ -796,7 +796,7 @@ def handle_set_design_rules(app: adsk.core.Application, args: dict) -> dict: return { "success": True,- "output": "3rd party fab design rules (from bundled .edru files)",+ "output": "Adom InstaPCB design rules (from bundled .edru files)", "data": { "2layer_rules": desc_2 or "2-layer .edru file not found", "4layer_rules": desc_4 or "4-layer .edru file not found",@@ -850,7 +850,7 @@ def handle_set_design_rules(app: adsk.core.Application, args: dict) -> dict: detection_method = detection["method"] # Resolve the .edru file: a fab pack the bridge staged (druPath, fetched from the wiki page- # adom/pcb-design-rules), else the bundled 3rd party fab rules for this layer count.+ # adom/pcb-design-rules), else the bundled Adom InstaPCB rules for this layer count. dru_path_arg = (args.get("druPath") or "").replace("\\", "/") if dru_path_arg: edru_path = dru_path_arg@@ -893,7 +893,7 @@ def handle_set_design_rules(app: adsk.core.Application, args: dict) -> dict: "message": ( (f"Design rules from {edru_filename} are now active. Rule violations will now show as " f"DRC markers in the board editor.") if dru_path_arg else- (f"3rd party fab {layer_count}-Layer design rules are now active. "+ (f"Adom InstaPCB {layer_count}-Layer design rules are now active. " f"Min trace: 0.08mm (3mil), min spacing: 0.08mm (3mil), min drill: 0.2mm (8mil). " f"Rule violations will now show as DRC markers in the board editor.") ),
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.11.9",+ "version": "1.11.10", "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", "homepage": "https://wiki.adom.inc/adom/fusion-bridge", "author": "Adom Inc.",
contracts/describe.json+4−3@@ -1,6 +1,6 @@ { "contract": "fusion-bridge verb catalog",- "frozen": "2026-09-23",+ "frozen": "2026-10-04", "note": "The Python bridge and the Rust bridge must both serve this catalog byte-for-byte in meaning: same names, same args, same result shapes, same timeouts. Regenerate ONLY from describe.py and diff in CI.", "source": "describe.py", "verbs": [@@ -545,9 +545,10 @@ ] }, "input": {+ "libraryPath": "optional str (.lbr on the box; required for the 3d view, defaults to the last library opened)", "packages": "required [<deviceset name>] (or 'package': single name)", "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)",- "views": "optional subset of ['symbol','footprint','3d','component'] (default ALL FOUR, in that assembly order - 3d is the dedicated full-size 3D-package render, component is the ganged view last)"+ "views": "optional subset of ['symbol','footprint','3d','component'] (default ALL FOUR, in that assembly order - 3d opens the part's BOUND f3d (footprint + chip) by its urn, frames it, captures it and closes it unsaved; needs libraryPath; component is the ganged view last)" }, "longRunning": true, "name": "fusion_capture_library_views",@@ -2692,7 +2693,7 @@ "pack": "dict {fab, vendor, process, layers, file, record}" }, "statusVerb": null,- "summary": "Load a FAB's design rules (DRC) into the open board. The rule packs live on the wiki page adom/pcb-design-rules (JLCPCB, PCBWay, OSH Park, Seeed, Eurocircuits, Aisler, Elecrow, Advanced Circuits, Sierra, MacroFab, NextPCB, ALLPCB and the 3rd party fab; one .edru per vendor/process/layer count, every number citing the vendor's own capabilities page): the bridge fetches the pack, stages it on the box, and the add-in runs `drc load`. {action:'list'} answers with every (fab, process, layers) row so you can offer the user a real choice; no fab means the bundled 3rd party fab rules for the board's layer count; druPath loads an .edru already on the Fusion machine; druText/druBase64 loads one that exists only on your side. Layers auto-detects from the open board. ALWAYS confirm with fusion_alerts afterwards: a refused file is an in-canvas toast, never a dialog.",+ "summary": "Load a FAB's design rules (DRC) into the open board. The rule packs live on the wiki page adom/pcb-design-rules (JLCPCB, PCBWay, OSH Park, Seeed, Eurocircuits, Aisler, Elecrow, Advanced Circuits, Sierra, MacroFab, NextPCB, ALLPCB and Adom InstaPCB; one .edru per vendor/process/layer count, every number citing the vendor's own capabilities page): the bridge fetches the pack, stages it on the box, and the add-in runs `drc load`. {action:'list'} answers with every (fab, process, layers) row so you can offer the user a real choice; no fab means the bundled Adom InstaPCB rules for the board's layer count (what ab's removed fusion_apply_instapcb_rules did); druPath loads an .edru already on the Fusion machine; druText/druBase64 loads one that exists only on your side. Layers auto-detects from the open board. ALWAYS confirm with fusion_alerts afterwards: a refused file is an in-canvas toast, never a dialog.", "timeoutSeconds": 90 }, {
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.11.9"+version = "1.11.10" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.11.9"+version = "1.11.10" edition = "2021" [dependencies]
describe.py+2−2@@ -203,7 +203,7 @@ _T = [ {"lbrPath": "required str (combined .lbr WINDOWS path to bind + open)", "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]", "outLbrPath": "optional str (default: overwrite lbrPath)", "capture": "optional bool (default true)", "projectId": "optional str", "folderId": "optional str", "openWhenDone": "optional bool (default true)"}, {"success": "bool", "boundLbr": "str", "partsBound": "int", "partsTotal": "int", "parts": "[{package,success,wip_urn,dims_mm,error}]", "screenshots": "[{package,stage,path}]", "opened": "bool"}, 1800, _STATUS, True, {"lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr", "parts": [{"package": "R_4k7", "lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step"}]}), ("fusion_capture_library_views", "Capture the LIBRARY-EDITOR views EEs trust: the schematic SYMBOL (full pinout), the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: symbol + the package table with the footprint<->package Mapped check + pin/pad counts). The 3D before/after shots don't show these. Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). Runs EDIT <pkg>.sym/.pac/.dev + WINDOW FIT + a background hwnd screenshot per view; returns the PNG paths on the box (C:/tmp/conduit-screenshots - pull with desktop_pull_file). EDIT uses the DEVICESET name (e.g. ESP32-S3FN8).",- {"packages": "required [<deviceset name>] (or 'package': single name)", "views": "optional subset of ['symbol','footprint','3d','component'] (default ALL FOUR, in that assembly order - 3d is the dedicated full-size 3D-package render, component is the ganged view last)", "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)"},+ {"packages": "required [<deviceset name>] (or 'package': single name)", "views": "optional subset of ['symbol','footprint','3d','component'] (default ALL FOUR, in that assembly order - 3d opens the part's BOUND f3d (footprint + chip) by its urn, frames it, captures it and closes it unsaved; needs libraryPath; component is the ganged view last)", "libraryPath": "optional str (.lbr on the box; required for the 3d view, defaults to the last library opened)", "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)"}, {"success": "bool", "captured": "[{package,view,path,ok}]"}, 300, _STATUS, True, {"packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT"]}), ("fusion_cleanup_cloud_files", "PRECISELY delete a list of cloud files by lineage urn - SAFE cleanup of AI-created clutter. Deletes ONLY the exact fileIds given (never name-guessing), so it can't touch a teammate's file in a shared folder. Loops server-side. Get the ids from fusion_aps_browse (item .id). Use this to remove f3d files the bridge created. A big list takes minutes (relay may time out at ~60s while deletes continue server-side - re-browse to confirm).", {"fileIds": "required [<lineage urn>]", "projectName": "optional str (default active project)", "folderPath": "optional str (default root - where the files live)"},@@ -297,7 +297,7 @@ _T = [ {"outputPath": "optional str", "grouped": "optional bool (default true)"}, {"componentCount": "int", "grouped": "bool"}, 60, None, False, {"outputPath": "C:/tmp/bom.csv"}), ("fusion_export_cpl", "Export the Component Placement List (pick-and-place) CSV.", {"outputPath": "optional str", "side": "optional str (all|top|bottom)"}, {"topCount": "int", "bottomCount": "int"}, 60, None, False, {"outputPath": "C:/tmp/cpl.csv"}),- ("fusion_set_design_rules", "Load a FAB's design rules (DRC) into the open board. The rule packs live on the wiki page adom/pcb-design-rules (JLCPCB, PCBWay, OSH Park, Seeed, Eurocircuits, Aisler, Elecrow, Advanced Circuits, Sierra, MacroFab, NextPCB, ALLPCB and the 3rd party fab; one .edru per vendor/process/layer count, every number citing the vendor's own capabilities page): the bridge fetches the pack, stages it on the box, and the add-in runs `drc load`. {action:'list'} answers with every (fab, process, layers) row so you can offer the user a real choice; no fab means the bundled 3rd party fab rules for the board's layer count; druPath loads an .edru already on the Fusion machine; druText/druBase64 loads one that exists only on your side. Layers auto-detects from the open board. ALWAYS confirm with fusion_alerts afterwards: a refused file is an in-canvas toast, never a dialog.",+ ("fusion_set_design_rules", "Load a FAB's design rules (DRC) into the open board. The rule packs live on the wiki page adom/pcb-design-rules (JLCPCB, PCBWay, OSH Park, Seeed, Eurocircuits, Aisler, Elecrow, Advanced Circuits, Sierra, MacroFab, NextPCB, ALLPCB and Adom InstaPCB; one .edru per vendor/process/layer count, every number citing the vendor's own capabilities page): the bridge fetches the pack, stages it on the box, and the add-in runs `drc load`. {action:'list'} answers with every (fab, process, layers) row so you can offer the user a real choice; no fab means the bundled Adom InstaPCB rules for the board's layer count (what ab's removed fusion_apply_instapcb_rules did); druPath loads an .edru already on the Fusion machine; druText/druBase64 loads one that exists only on your side. Layers auto-detects from the open board. ALWAYS confirm with fusion_alerts afterwards: a refused file is an in-canvas toast, never a dialog.", {"fab": "optional str vendorSlug (jlcpcb|pcbway|osh-park|seeed-fusion|eurocircuits|aisler|elecrow|advanced-circuits-4pcb|sierra-circuits|macrofab|nextpcb|allpcb|adom-instapcb)", "process": "optional str (the vendor's tier: standard|advanced|proto|enig|hasl|...; default standard)", "layers": "optional str (auto|2|4|6)", "druPath": "optional str .edru/.dru ALREADY on the Fusion machine, loaded as-is", "druText": "optional str: the whole .edru/.dru file as text, for rules that exist only on YOUR side (a container). The bridge stages it on the box and loads it (1.10.37; replaces ab's removed fusion_load_design_rules)", "druBase64": "optional str: the same, base64-encoded", "druName": "optional str: file name for the staged copy", "action": "optional str (apply|list|export|show)"}, {"activeWorkspace": "str", "description": "str", "pack": "dict {fab, vendor, process, layers, file, record}"}, 90, None, False, {"fab": "jlcpcb", "layers": "2"}), ("fusion_detect_layers", "Detect 2-layer vs 4-layer board (ULP + CAM comparison).", {}, {"layerCount": "int", "copperLayers": "[int]"}, 90, None, False, {}),
docs/board-layout-2d.md+3−3@@ -18,7 +18,7 @@ Opening the `.brd` child directly gives an isolated view. See [schematics](schem ```bash adom-bridge fusion_set_design_rules '{"action":"list"}' # every fab, process, layers row adom-bridge fusion_set_design_rules '{"fab":"jlcpcb","process":"standard","layers":"2"}'-adom-bridge fusion_set_design_rules '{}' # the bundled 3rd party fab rules+adom-bridge fusion_set_design_rules '{}' # the bundled Adom InstaPCB rules adom-bridge fusion_set_design_rules '{"druPath":"C:/tmp/house.edru"}' # a local rule file, as-is adom-bridge fusion_alerts '{}' # confirm the load ```@@ -26,14 +26,14 @@ adom-bridge fusion_alerts '{}' `fusion_set_design_rules` loads a fab's rules into the open board. The rule packs live on the wiki page [adom/pcb-design-rules](https://wiki.adom.inc/adom/pcb-design-rules): JLCPCB, PCBWay, OSH Park, Seeed, Eurocircuits, Aisler, Elecrow, Advanced Circuits, Sierra, MacroFab, NextPCB, ALLPCB-and the 3rd party fab, one `.edru` per vendor, process and layer count, every number citing the+and Adom InstaPCB, one `.edru` per vendor, process and layer count, every number citing the vendor's own capabilities page. The bridge fetches the pack, stages it on the box, and the add-in runs `drc load`. `layers` auto-detects from the open board. `{action:"list"}` answers on a machine without Fusion, so you can offer the user a real choice of fab before a board is open. `apply` and `export` need a board open and say so themselves. Read `ruleWarnings` in the reply. The bridge scans the downloaded profile and warns when it carries-a same-signal Copper Clearance rule over an all/all or trace scope. The 3rd party fab 2L profile, like+a same-signal Copper Clearance rule over an all/all or trace scope. The InstaPCB 2L profile, like the other generated profiles, carries "Same-net track spacing" (0.25 mm, all/all, samesignal=yes). On the six-net test fixture native DRC reported that one rule as 31 Overlap warnings, at the 13 pad entries and 18 trace and via joints. With the rule disabled the identical board is clean. Use
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.11.9"+DASH_VERSION="1.11.10" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.11.9",+ "version": "1.11.10", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+10−26@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.11.9",+ "version": "1.11.10", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -76,6 +80,9 @@ "add a 3d model to a component", "make a fusion 360 library with 3d", "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page", "aps not configured", "share aps client id across my org", "set up fusion cloud search for my company",@@ -108,28 +115,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "agent_permissions": {- "allow_bash": [],- "allow": [- "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",- "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",- "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",- "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."- ],- "soft_deny": [- "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",- "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",- "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",- "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."- ],- "environment": [- "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",- "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."- ]- },- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"] # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.11.9"+version = "1.11.10" edition = "2021" license = "MIT"
rust/bridge/src/orchestrations.rs+60−8@@ -825,18 +825,47 @@ fn capture_3d_view(ctx: &Ctx, pkg: &str, lbr_path: &str, settle: f64, pass: &dyn } } }- let cp = if lbr_path.is_empty() { json!({"success": false, "error": "no libraryPath"}) } else { create_3d_package(ctx, lbr_path, pass) };- if cp["success"].as_bool() != Some(true) { entry["errorCode"] = json!("create_3d_package_failed"); entry["error"] = json!(format!("Could not enter the Package3D environment: {}", cp["error"].as_str().unwrap_or("None").chars().take(200).collect::<String>())); return entry; }+ // Photograph the BOUND 3D asset, not Fusion's Create3DPackage generator: the generator loads+ // only the footprint (no chip body) and always for the library's FIRST package, and its+ // Package3DStop leaves an unsaved generator tab behind (#109). The bound f3d is the real+ // artifact (footprint + chip), opened by its urn, framed, captured and closed unsaved.+ if lbr_path.is_empty() { entry["errorCode"] = json!("library_path_required"); entry["error"] = json!("The 3d view needs libraryPath to find the part's bound 3D package."); return entry; }+ let Some((p3d_pkg, urn)) = std::fs::read_to_string(lbr_path).ok().and_then(|x| bound_p3d_urn(&x, pkg)) else {+ entry["errorCode"] = json!("no_3d_package_bound"); entry["error"] = json!(format!("{pkg} has no <package3dinstance> in that library, so there is no 3D body to show."));+ entry["_hint"] = json!("fusion_build_library_3d (or fusion_bind_library_3d) binds one; then re-capture."); return entry;+ };+ entry["package3d"] = json!(p3d_pkg); entry["urn"] = json!(urn);+ let mut opened = Value::Null;+ for attempt in 0..2 { opened = pass("open_by_urn", json!({"urn": urn})); if opened["success"].as_bool() == Some(true) { break; } if attempt == 0 { std::thread::sleep(Duration::from_secs(10)); } }+ if opened["success"].as_bool() != Some(true) {+ entry["errorCode"] = json!("open_3d_failed"); entry["error"] = json!(format!("Could not open the bound f3d: {}", opened["error"].as_str().unwrap_or("None").chars().take(160).collect::<String>()));+ let _ = ensure_library_active(ctx, lbr_path, 30, pass); return entry;+ }+ let _ = pass("run_modeling_script", json!({"script": "import adsk.core, time\napp=adsk.core.Application.get()\ntry:\n w=app.userInterface.workspaces.itemById('FusionSolidEnvironment')\n if w and not w.isActive: w.activate()\n adsk.doEvents(); time.sleep(1)\nexcept Exception: pass\ntry:\n app.activeViewport.goHome(False)\n app.activeViewport.fit()\nexcept Exception: pass\nresult={}\n"})); if settle > 0.0 { std::thread::sleep(Duration::from_secs_f64(settle)); }+ let (bodies, _) = crate::prove::count_3d_bodies(pass);+ entry["bodies"] = json!(bodies); entry["path"] = json!(capture_labeled(ctx, &format!("{pkg}_3d")));- entry["ok"] = json!(!entry["path"].is_null());- let _ = pass("execute_text_command", json!({"command": "Package3DStop"}));- std::thread::sleep(Duration::from_millis(600));- let d = dismiss_dialogs_bg(ctx);- if !d.is_empty() { eprintln!("[fusion-bridge] Package3DStop raised a dialog ({}) - answered in the background; see skills/fusion-no-modal-dialogs", d.join(", ")); }+ entry["ok"] = json!(!entry["path"].is_null() && bodies != 0);+ if bodies == 0 { entry["errorCode"] = json!("no_bodies"); entry["error"] = json!("The bound f3d opened but holds no bodies, so the frame shows no chip."); }+ // close the f3d unsaved (it is the stored asset, nothing here edited it), then put the library back in front+ let _ = pass("run_modeling_script", json!({"script": "import adsk.core, time\napp=adsk.core.Application.get()\nfor _ in range(12):\n if not app.hasActiveJobs: break\n time.sleep(2)\napp.activeDocument.close(False)\nresult={}"}));+ let _ = ensure_library_active(ctx, lbr_path, 45, pass); entry } +/// The bound 3D package of a deviceset: (package name, urn) from its device's package3dinstance,+/// falling back to the <package3d> whose packageinstance names the device's package.+pub fn bound_p3d_urn(xml: &str, deviceset: &str) -> Option<(String, String)> {+ let doc = roxmltree::Document::parse_with_options(xml, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }).ok()?;+ let ds = doc.descendants().find(|n| n.has_tag_name("deviceset") && n.attribute("name").map(|v| v.eq_ignore_ascii_case(deviceset)).unwrap_or(false))?;+ let dv = ds.descendants().find(|n| n.has_tag_name("device") && n.attribute("package").map(|p| !p.is_empty()).unwrap_or(false))?;+ let pkg = dv.attribute("package").unwrap_or("").to_string();+ if let Some(u) = dv.descendants().find(|n| n.has_tag_name("package3dinstance")).and_then(|n| n.attribute("package3d_urn")).map(str::trim).filter(|u| !u.is_empty()) { return Some((pkg, u.to_string())); }+ doc.descendants().filter(|n| n.has_tag_name("package3d")).find(|p3| p3.descendants().any(|n| n.has_tag_name("packageinstance") && n.attribute("name").map(|v| v.eq_ignore_ascii_case(&pkg)).unwrap_or(false)))+ .and_then(|p3| p3.attribute("urn").filter(|u| !u.trim().is_empty()).or(p3.attribute("wip_urn")).map(|u| (pkg.clone(), u.trim().to_string())))+}+ /// `fusion_capture_library_views`: symbol, footprint, 3D and component views of each part. pub fn capture_library_views(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value { let pkgs: Vec<String> = args["packages"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).filter(|v: &Vec<String>| !v.is_empty()).or_else(|| args["package"].as_str().map(|p| vec![p.to_string()])).unwrap_or_default();@@ -1022,7 +1051,7 @@ pub fn schematic(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) - let capture = args["capture"].clone(); let menu = "fusion_schematic takes action:\"open\" with ONE of: fileName (+projectName) for a cloud design, urn for a cloud design by lineage, query to find-and-open by name, schPath for a local EAGLE .sch (its sibling .brd is imported too unless withBoard:false), or filePath for a Fusion-native .fsch; action:\"create\" with an optional name for a new blank schematic (dialog-free); action:\"show\" to switch the open design to its schematic view. A schematic is a VIEW of an electronics design, never a file on its own."; match action.as_str() {- "show" => { let mut r = pass("show_schematic", json!({"capture": capture})); r["via"] = json!("show_schematic"); r }+ "show" => { let mut r = show_with_document("show_schematic", &json!({"capture": capture, "documentName": args["documentName"], "name": args["name"]}), pass); r["via"] = json!("show_schematic"); r } "create" => { let name: String = a("name").unwrap_or_else(|| format!("adom-schematic-{}", (now_f() as u64) % 100000)).chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').collect(); let root = match crate::fusionpath::stage_root(&ctx.state_dir) { Ok(r) => r.join("new"), Err(e) => return json!({"success": false, "errorCode": "stage_failed", "error": e}) };@@ -1380,3 +1409,26 @@ mod png_tests { let _ = std::fs::remove_dir_all(&dir); } }++#[cfg(test)]+mod bound_p3d_tests {+ const LBR: &str = r#"<?xml version="1.0"?><eagle><drawing><library>+<packages3d>+<package3d name="A" urn="" wip_urn="urn:adsk.wipprod:fs.file:vf.AAA?version=1" type="model"><packageinstances><packageinstance name="A"/></packageinstances></package3d>+<package3d name="B" urn="" wip_urn="urn:adsk.wipprod:fs.file:vf.BBB?version=1" type="model"><packageinstances><packageinstance name="B"/></packageinstances></package3d>+</packages3d>+<devicesets>+<deviceset name="PartA"><devices><device name="" package="A"><package3dinstances><package3dinstance package3d_urn="urn:adsk.wipprod:fs.file:vf.AAA?version=1"/></package3dinstances></device></devices></deviceset>+<deviceset name="PartB"><devices><device name="" package="B"></device></devices></deviceset>+<deviceset name="PartC"><devices><device name="" package="C"></device></devices></deviceset>+</devicesets></library></drawing></eagle>"#;+ #[test]+ fn picks_each_parts_own_binding() {+ // the device's own instance wins; this is the part asked for, never the library's first package+ assert_eq!(super::bound_p3d_urn(LBR, "parta"), Some(("A".into(), "urn:adsk.wipprod:fs.file:vf.AAA?version=1".into())));+ // no instance on the device: the package3d whose packageinstance names its package+ assert_eq!(super::bound_p3d_urn(LBR, "PartB"), Some(("B".into(), "urn:adsk.wipprod:fs.file:vf.BBB?version=1".into())));+ assert_eq!(super::bound_p3d_urn(LBR, "PartC"), None);+ assert_eq!(super::bound_p3d_urn(LBR, "Missing"), None);+ }+}
rust/bridge/src/services.rs+2−2@@ -253,7 +253,7 @@ pub fn set_design_rules(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> V } let mut rule_warnings: Vec<Value> = Vec::new(); if action == "list" {- let idx = match design_rules_index(&ctx.state_dir) { Ok(i) => i, Err(e) => return json!({"success": false, "errorCode": "design_rules_page_unreachable", "error": e.chars().take(200).collect::<String>(), "_hint": "The wiki page adom/pcb-design-rules did not answer; the bundled 3rd party fab rules still apply with no fab argument."}) };+ let idx = match design_rules_index(&ctx.state_dir) { Ok(i) => i, Err(e) => return json!({"success": false, "errorCode": "design_rules_page_unreachable", "error": e.chars().take(200).collect::<String>(), "_hint": "The wiki page adom/pcb-design-rules did not answer; the bundled Adom InstaPCB rules still apply with no fab argument."}) }; let rows: Vec<Value> = idx["vendors"].as_array().cloned().unwrap_or_default().iter().map(|v| json!({"fab": v["vendorSlug"], "vendor": v["vendor"], "process": v["process"], "layers": v["layerCount"], "available": v["filesGenerated"].as_bool().unwrap_or(false)})).collect(); return json!({"success": true, "rules": rows, "count": rows.len(), "page": "https://wiki.adom.inc/adom/pcb-design-rules", "_hint": "Pick a row and call fusion_set_design_rules {fab, process, layers}. Layers auto-detects when omitted (2 or 4 from the open board)."}); }@@ -263,7 +263,7 @@ pub fn set_design_rules(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> V let mut layers = match &a["layers"] { Value::Null => "auto".to_string(), v => crate::fusion_prefs::py_str(v) }; if layers == "auto" { let det = pass("detect_layers", json!({})); layers = det["data"]["layerCount"].as_i64().or(det["layerCount"].as_i64()).unwrap_or(2).to_string(); } let layers_n: i64 = layers.parse().unwrap_or(2);- let idx = match design_rules_index(&ctx.state_dir) { Ok(i) => i, Err(e) => return json!({"success": false, "errorCode": "design_rules_page_unreachable", "error": e.chars().take(200).collect::<String>(), "_hint": "Could not fetch the rule packs from wiki.adom.inc; pass druPath to a local .edru, or omit fab for the bundled 3rd party fab rules."}) };+ let idx = match design_rules_index(&ctx.state_dir) { Ok(i) => i, Err(e) => return json!({"success": false, "errorCode": "design_rules_page_unreachable", "error": e.chars().take(200).collect::<String>(), "_hint": "Could not fetch the rule packs from wiki.adom.inc; pass druPath to a local .edru, or omit fab for the bundled Adom InstaPCB rules."}) }; let row = match design_rules_pick(&idx, &fab, a["process"].as_str().unwrap_or(""), layers_n) { Ok(r) => r, Err(alt) => return json!({"success": false, "errorCode": "design_rules_not_found", "fab": fab, "layers": layers_n, "available": alt, "_hint": "No pack for that fab/process/layer count; `available` lists what the page has. fusion_set_design_rules {action:'list'} shows every row."}) }; if row["filesGenerated"].as_bool() != Some(true) { return json!({"success": false, "errorCode": "design_rules_not_generated", "fab": fab, "process": row["process"], "layers": layers_n, "_hint": "The page has this vendor's record but no generated .edru yet (the vendor publishes no usable limits for it). Pick another process or fab."}); } let rel = row["files"]["fusionEdru"].as_str().unwrap_or("").to_string();
server.py+6−6@@ -14697,7 +14697,7 @@ def _design_rules_index(max_age_s: int = 3600) -> dict: def _design_rules_pick(idx: dict, fab: str, process: str, layers: int): """The index row for (fab, process, layers). fab matches vendorSlug or a loose alias- (jlcpcb, pcbway, oshpark...); process defaults to the vendor's standard tier."""+ (jlcpcb, pcbway, instapcb, oshpark...); process defaults to the vendor's standard tier.""" want = re.sub(r"[^a-z0-9]", "", (fab or "").lower()) rows = [v for v in idx.get("vendors") or [] if re.sub(r"[^a-z0-9]", "", v.get("vendorSlug", "").lower()) == want or want and want in re.sub(r"[^a-z0-9]", "", v.get("vendorSlug", "").lower())]@@ -14716,8 +14716,8 @@ def _handle_set_design_rules(fusion_info: dict, args: dict) -> dict: rules into the open board. The rule packs live on the wiki page adom/pcb-design-rules (one JSON record per vendor/process/layer count, generated .edru per row, every number citing the vendor's own capabilities page); the bridge fetches the .edru, stages it on- the box, and the add-in runs `drc load <path>`. No fab (or the bundled fab with the page- unreachable) keeps the bundled 3rd party fab behaviour. action:"list" answers with the+ the box, and the add-in runs `drc load <path>`. No fab (or fab:"instapcb" with the page+ unreachable) keeps the bundled Adom InstaPCB behaviour. action:"list" answers with the vendors, processes and layer counts on the page so the AI can offer a real choice. AI hint: the pack is per (vendor, process, layers). "jlcpcb" alone means its standard@@ -14734,13 +14734,13 @@ def _handle_set_design_rules(fusion_info: dict, args: dict) -> dict: idx = _design_rules_index() except Exception as e: return {"success": False, "errorCode": "design_rules_page_unreachable", "error": str(e)[:200],- "_hint": "The wiki page adom/pcb-design-rules did not answer; the bundled 3rd party fab rules still apply with no fab argument."}+ "_hint": "The wiki page adom/pcb-design-rules did not answer; the bundled Adom InstaPCB rules still apply with no fab argument."} rows = [{"fab": v.get("vendorSlug"), "vendor": v.get("vendor"), "process": v.get("process"), "layers": v.get("layerCount"), "available": bool(v.get("filesGenerated"))} for v in idx.get("vendors") or []] return {"success": True, "rules": rows, "count": len(rows), "page": "https://wiki.adom.inc/adom/pcb-design-rules", "_hint": "Pick a row and call fusion_set_design_rules {fab, process, layers}. Layers auto-detects when omitted (2 or 4 from the open board)."} if action in ("show", "export") or (not fab and not dru_path):- return _proxy_to_addin("set_design_rules", a, timeout=90) # the bundled 3rd party fab path, unchanged+ return _proxy_to_addin("set_design_rules", a, timeout=90) # the bundled InstaPCB path, unchanged if not dru_path: layers = str(a.get("layers") or "auto") if layers == "auto":@@ -14750,7 +14750,7 @@ def _handle_set_design_rules(fusion_info: dict, args: dict) -> dict: idx = _design_rules_index() except Exception as e: return {"success": False, "errorCode": "design_rules_page_unreachable", "error": str(e)[:200],- "_hint": "Could not fetch the rule packs from wiki.adom.inc; pass druPath to a local .edru, or omit fab for the bundled 3rd party fab rules."}+ "_hint": "Could not fetch the rule packs from wiki.adom.inc; pass druPath to a local .edru, or omit fab for the bundled Adom InstaPCB rules."} row, alt = _design_rules_pick(idx, fab, a.get("process") or "", int(layers)) if not row: return {"success": False, "errorCode": "design_rules_not_found", "fab": fab, "layers": int(layers),
skills/fusion-electronics/SKILL.md+1−1@@ -136,7 +136,7 @@ fusion_set_design_rules {"action": "list"} // every (fab, fusion_set_design_rules {"fab": "jlcpcb", "layers": "2"} // standard tier; layers auto-detects when omitted fusion_set_design_rules {"fab": "pcbway", "process": "advanced", "layers": "4"} fusion_set_design_rules {"druPath": "C:/Users/me/my-fab.edru"} // a local file, as-is-fusion_set_design_rules {} // the bundled 3rd party fab rules (unchanged)+fusion_set_design_rules {} // the bundled Adom InstaPCB rules (unchanged) ``` Read `ruleWarnings` in the apply reply (1.10.3+): it names any rule in the downloaded profile
tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.sh