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 162 file(s)
19 files changed
+220−50
BRIDGE_VERSION+1−1addin/AdomBridge/commands/open_electronics_file.py+63−10bridge.json+1−1dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1install.sh+1−1package.json+1−1page.json+11−27rust/Cargo.toml+1−1rust/PORTING.md+8rust/bridge/src/addin.rs+4−2rust/bridge/src/fusionpath.rs+117rust/bridge/src/main.rs+1rust/bridge/src/orchestrations.rs+5−2rust/bridge/src/testproject.rs+4−2tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.shBRIDGE_VERSION+1−1@@ -1 +1 @@-1.10.10+1.10.11
addin/AdomBridge/commands/open_electronics_file.py+63−10@@ -19,6 +19,62 @@ import os import time ++_SIBLING_EXTS = ("sch", "brd", "lbr", "step", "stp", "f3d", "edru", "dru", "flbr", "fsch", "fbrd", "ulp")+++def _stage_root():+ """The first space-free, writable stage root (a stage under a path with a space is useless)."""+ import os+ cands = [os.path.join(os.path.expanduser("~"), ".adom", "fusion-bridge", "stage")]+ for env in ("LOCALAPPDATA", "TEMP"):+ v = os.environ.get(env, "")+ if v:+ cands.append(os.path.join(v, "adom-fusion-stage"))+ cands.append("C:/adom-fusion-stage")+ for c in cands:+ c = c.replace("\\", "/")+ if " " in c:+ continue+ try:+ os.makedirs(c, exist_ok=True)+ return c+ except Exception:+ continue+ return None+++def _stage_for_fusion(file_path):+ """Copy the file (and its same-stem siblings) under a space-free folder; return the copy's path."""+ import os, shutil, hashlib+ src = file_path.replace("\\", "/")+ if not os.path.isfile(src):+ return None+ root = _stage_root()+ if not root:+ return None+ parent = os.path.dirname(src)+ bucket = hashlib.sha1(parent.lower().encode("utf-8")).hexdigest()[:8]+ d = os.path.join(root, bucket).replace("\\", "/")+ os.makedirs(d, exist_ok=True)+ stem = os.path.splitext(os.path.basename(src))[0].lower()+ for name in os.listdir(parent):+ q = os.path.join(parent, name)+ if not os.path.isfile(q):+ continue+ s, ext = os.path.splitext(name)+ if q.replace("\\", "/") == src or (s.lower() == stem and ext.lstrip(".").lower() in _SIBLING_EXTS):+ dst = os.path.join(d, name)+ try:+ if not (os.path.exists(dst) and os.path.getsize(dst) == os.path.getsize(q) and os.path.getmtime(dst) >= os.path.getmtime(q)):+ shutil.copy2(q, dst)+ except Exception:+ if q.replace("\\", "/") == src:+ raise+ out = os.path.join(d, os.path.basename(src)).replace("\\", "/")+ return out if os.path.isfile(out) else None++ def _open_local_document(app, file_path: str): """Run Document.newDesignFromLocal for a path that MAY CONTAIN SPACES. @@ -32,8 +88,10 @@ def _open_local_document(app, file_path: str): Two-step, so it works whichever way this Fusion build's parser behaves: 1. QUOTE the path.- 2. If that fails and the path has spaces, retry with the Windows 8.3 SHORT path- (GetShortPathNameW) - same file, no spaces, no copying.+ 2. If that fails and the path has spaces, retry with a STAGED copy of the file (plus its+ same-stem siblings) under a space-free folder. Never the 8.3 short path: Fusion titles+ the document after the path it is given, and "0603WA~1" broke every name check+ (bridge 1.10.11, issue #83). Returns (result, how) and raises the LAST exception if every attempt fails. """@@ -42,14 +100,9 @@ def _open_local_document(app, file_path: str): attempts.append((quoted, "quoted")) if " " in file_path: try:- import ctypes- from ctypes import wintypes- _gsp = ctypes.windll.kernel32.GetShortPathNameW- _gsp.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]- _gsp.restype = wintypes.DWORD- buf = ctypes.create_unicode_buffer(600)- if _gsp(file_path, buf, 600) and buf.value and " " not in buf.value:- attempts.append((buf.value, "shortpath"))+ staged = _stage_for_fusion(file_path)+ if staged and " " not in staged:+ attempts.append((staged, "staged")) except Exception: pass def _doc_name():
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.10.10",+ "version": "1.10.11", "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", "homepage": "https://wiki.adom.inc/adom/fusion-bridge", "author": "Adom Inc.",
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.10.10"+version = "1.10.11" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.10"+version = "1.10.11" edition = "2021" [dependencies]
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.10.10"+DASH_VERSION="1.10.11" 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.10.10",+ "version": "1.10.11", "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+11−27@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.10.10",+ "version": "1.10.11", "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",@@ -75,7 +79,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -104,28 +111,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.10.10"+version = "1.10.11" edition = "2021" license = "MIT"
rust/PORTING.md+8@@ -605,3 +605,11 @@ port learned: the post-envelope (polled for 1.2 s after any window-raising verb) and by the sentinel, and carried in the routing reply with a checkpoint hint; the document snapshot behind it is a 3 s undiagnosed hop so a hung main thread costs seconds, not the relay's whole minute.++## Never an 8.3 path (2026-09-10)++Fusion's text commands cannot take a path with a space and reject the quoted form, and the old+answer (GetShortPathNameW) made Fusion title documents "0603WA~1", which silently broke every+name check. `fusionpath::for_fusion` stages the file and its siblings into a space-free folder+instead; `output_for_fusion` + `finish_output` do the same for files Fusion writes. If a new+verb hands Fusion a path, go through those, never through `short_path`.
rust/bridge/src/addin.rs+4−2@@ -200,7 +200,9 @@ pub struct Ctx<'a> { pub host: &'a dyn fusion_core::HostProcess, /// `HostPaths::fusion_executable()` at call time (None = not installed) pub fusion_exe: Option<std::path::PathBuf>,- /// `HostPaths::short_path` (the 8.3 form Fusion's text commands need)+ /// `HostPaths::short_path`. Unused since 1.10.11: paths with spaces are STAGED into a+ /// space-free folder (`fusionpath`) instead of shortened, because Fusion titles a document+ /// after the path it is given and the 8.3 alias ("0603WA~1") broke every name check. pub short_path: fn(&str) -> String, pub state_dir: std::path::PathBuf, pub caller: fusion_core::ab::Caller,@@ -211,7 +213,7 @@ pub struct Ctx<'a> { pub dialogs: &'a dyn Fn(Value) -> Value, } -impl<'a> Ctx<'a> { pub fn host_paths_short(&self, p: &str) -> String { (self.short_path)(p) } }+impl<'a> Ctx<'a> { #[deprecated(note = "stage with crate::fusionpath::for_fusion; never hand Fusion an 8.3 alias")] #[allow(dead_code)] pub fn host_paths_short(&self, p: &str) -> String { (self.short_path)(p) } } /// The whole pass-through for one verb. pub fn pass_through(cmd: &str, args: &Value, ctx: &Ctx) -> Value {
rust/bridge/src/fusionpath.rsadded+117@@ -0,0 +1,117 @@+//! Paths Fusion's text commands can take.+//!+//! `executeTextCommand` is EAGLE's space-delimited command line and its quoted form fails, so a+//! file that lives under a path with a space (the bridge cache under "Adom Bridge", a profile+//! like "John Lauer", "Program Files") is STAGED: copied, with its same-stem siblings, into a+//! space-free folder, and Fusion is handed the copy. Never the DOS 8.3 alias: Fusion titles the+//! document after the path it was given, and "0603WA~1" broke every name check (issue #83).+use std::path::{Path, PathBuf};++/// Files that travel with the one being opened (a .sch wants its .brd beside it, a mint wants+/// the .step next to the .lbr).+pub const SIBLING_EXTS: &[&str] = &["sch", "brd", "lbr", "step", "stp", "f3d", "edru", "dru", "flbr", "fsch", "fbrd", "ulp"];++fn has_space(p: &Path) -> bool { p.to_string_lossy().contains(' ') }++/// The first space-free, writable stage root: the bridge state dir, then LOCALAPPDATA, then the+/// drive root. A stage under a path with a space would defeat the whole point.+pub fn stage_root(state_dir: &Path) -> Result<PathBuf, String> {+ let mut candidates: Vec<PathBuf> = vec![state_dir.join("stage")];+ if let Ok(la) = std::env::var("LOCALAPPDATA") { if !la.trim().is_empty() { candidates.push(PathBuf::from(la).join("adom-fusion-stage")); } }+ if let Ok(t) = std::env::var("TEMP") { if !t.trim().is_empty() { candidates.push(PathBuf::from(t).join("adom-fusion-stage")); } }+ candidates.push(PathBuf::from(if cfg!(windows) { "C:/adom-fusion-stage" } else { "/tmp/adom-fusion-stage" }));+ for c in candidates {+ if has_space(&c) { continue; }+ if std::fs::create_dir_all(&c).is_ok() { return Ok(c); }+ }+ Err("no space-free folder is writable for staging a file Fusion can open (state dir, LOCALAPPDATA, TEMP and the drive root all failed)".into())+}++fn bucket(parent: &Path) -> String {+ use std::hash::{Hash, Hasher};+ let mut h = std::collections::hash_map::DefaultHasher::new();+ parent.to_string_lossy().to_lowercase().replace('\\', "/").hash(&mut h);+ format!("{:08x}", h.finish() as u32)+}++fn fwd(p: &Path) -> String { p.to_string_lossy().replace('\\', "/") }++/// The path to hand Fusion for an INPUT file: the file itself when its path has no space,+/// otherwise a staged copy (siblings included) under a space-free folder. The document Fusion+/// opens keeps its real name either way.+pub fn for_fusion(state_dir: &Path, path: &str) -> Result<String, String> {+ let p = PathBuf::from(path.replace('\\', "/"));+ if !has_space(&p) { return Ok(fwd(&p)); }+ if !p.is_file() { return Err(format!("{} does not exist, so it cannot be staged for Fusion", fwd(&p))); }+ let parent = p.parent().map(Path::to_path_buf).unwrap_or_default();+ let dir = stage_root(state_dir)?.join(bucket(&parent));+ std::fs::create_dir_all(&dir).map_err(|e| format!("cannot create stage folder {}: {e}", fwd(&dir)))?;+ let stem = p.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();+ let mut copied = Vec::new();+ if let Ok(rd) = std::fs::read_dir(&parent) {+ for e in rd.flatten() {+ let q = e.path();+ if !q.is_file() { continue; }+ let same_stem = q.file_stem().map(|s| s.to_string_lossy().eq_ignore_ascii_case(&stem)).unwrap_or(false);+ let ext_ok = q.extension().map(|x| SIBLING_EXTS.contains(&x.to_string_lossy().to_lowercase().as_str())).unwrap_or(false);+ if q == p || (same_stem && ext_ok) {+ let dst = dir.join(q.file_name().unwrap());+ let fresh = match (std::fs::metadata(&q), std::fs::metadata(&dst)) { (Ok(a), Ok(b)) => a.len() == b.len() && a.modified().ok() <= b.modified().ok(), _ => false };+ if !fresh { std::fs::copy(&q, &dst).map_err(|e| format!("cannot stage {} for Fusion: {e}", fwd(&q)))?; }+ copied.push(dst);+ }+ }+ }+ let staged = dir.join(p.file_name().unwrap());+ if !staged.is_file() { return Err(format!("staging {} produced no file", fwd(&p))); }+ Ok(fwd(&staged))+}++/// For an OUTPUT path Fusion will write: hand Fusion a space-free target when the wanted one+/// has a space, and return the staged path to move afterwards with `finish_output`.+pub fn output_for_fusion(state_dir: &Path, out: &str) -> Result<(String, Option<String>), String> {+ let p = PathBuf::from(out.replace('\\', "/"));+ if !has_space(&p) { return Ok((fwd(&p), None)); }+ let dir = stage_root(state_dir)?.join("out");+ std::fs::create_dir_all(&dir).map_err(|e| format!("cannot create stage folder {}: {e}", fwd(&dir)))?;+ let staged = dir.join(p.file_name().ok_or("output path has no file name")?);+ let _ = std::fs::remove_file(&staged);+ Ok((fwd(&staged), Some(fwd(&staged))))+}++/// Move a staged output to where the caller asked for it.+pub fn finish_output(staged: &str, out: &str) -> Result<(), String> {+ if let Some(parent) = Path::new(out).parent() { let _ = std::fs::create_dir_all(parent); }+ std::fs::copy(staged, out).map_err(|e| format!("cannot move the staged output to {out}: {e}"))?;+ let _ = std::fs::remove_file(staged);+ Ok(())+}++#[cfg(test)]+mod tests {+ use super::*;+ #[test]+ fn spaced_inputs_are_staged_with_siblings_and_named_the_same() {+ let root = std::env::temp_dir().join(format!("fusionpath-{}", std::process::id()));+ let src = root.join("Adom Bridge").join("samples"); std::fs::create_dir_all(&src).unwrap();+ std::fs::write(src.join("0603WAF3300T5E.lbr"), b"<eagle/>").unwrap();+ std::fs::write(src.join("0603WAF3300T5E.step"), b"ISO-10303").unwrap();+ std::fs::write(src.join("other.step"), b"x").unwrap();+ let state = root.join("state");+ let got = for_fusion(&state, &src.join("0603WAF3300T5E.lbr").to_string_lossy()).unwrap();+ assert!(!got.contains(' '), "{got}");+ assert!(got.ends_with("/0603WAF3300T5E.lbr"), "{got}");+ let dir = Path::new(&got).parent().unwrap();+ assert!(dir.join("0603WAF3300T5E.step").is_file(), "sibling staged");+ assert!(!dir.join("other.step").exists(), "unrelated file not staged");+ let plain = root.join("plain"); std::fs::create_dir_all(&plain).unwrap(); std::fs::write(plain.join("a.brd"), b"y").unwrap();+ assert_eq!(for_fusion(&state, &plain.join("a.brd").to_string_lossy()).unwrap(), plain.join("a.brd").to_string_lossy().replace('\\', "/"));+ assert!(for_fusion(&state, &src.join("missing.lbr").to_string_lossy()).is_err());+ let (o, staged) = output_for_fusion(&state, &root.join("out dir").join("lib.flbr").to_string_lossy()).unwrap();+ assert!(!o.contains(' ') && staged.is_some());+ std::fs::write(&o, b"flbr").unwrap();+ finish_output(&o, &root.join("out dir").join("lib.flbr").to_string_lossy()).unwrap();+ assert!(root.join("out dir").join("lib.flbr").is_file());+ let _ = std::fs::remove_dir_all(&root);+ }+}
rust/bridge/src/main.rs+1@@ -39,6 +39,7 @@ mod addin_install; mod janitor; mod electronics; mod routing;+mod fusionpath; mod tour; mod palette; use fusion_core::ab::Caller;
rust/bridge/src/orchestrations.rs+5−2@@ -119,7 +119,8 @@ pub fn open_lbr(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> let verify = args["verify"].as_bool().unwrap_or(false); crate::chains::note_bridge_doc_pub(&ctx.state_dir, &file_path); let mut results: Vec<String> = Vec::new();- let open_result = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", ctx.host_paths_short(&file_path))}));+ let fusion_path = match crate::fusionpath::for_fusion(&ctx.state_dir, &file_path) { Ok(p) => p, Err(e) => return json!({"success": false, "errorCode": "stage_failed", "error": e}) };+ let open_result = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", fusion_path)})); if open_result["success"].as_bool() != Some(true) { let expected = stem.to_lowercase(); let mut opened_anyway = false;@@ -756,8 +757,10 @@ pub fn save_lbr(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> if out.is_empty() { return json!({"success": false, "error": "No outputPath specified"}); } if !out.to_lowercase().ends_with(".flbr") { out.push_str(".flbr"); } if let Some(parent) = std::path::Path::new(&out).parent() { if !parent.as_os_str().is_empty() && !parent.exists() { if let Err(e) = std::fs::create_dir_all(parent) { return json!({"success": false, "error": format!("Cannot create directory {}: {e}", parent.to_string_lossy())}); } } }- let r = pass("execute_text_command", json!({"command": format!("Document.CopyToDesktop {}", ctx.host_paths_short(&out))}));+ let (target, staged) = match crate::fusionpath::output_for_fusion(&ctx.state_dir, &out) { Ok(t) => t, Err(e) => return json!({"success": false, "errorCode": "stage_failed", "error": e}) };+ let r = pass("execute_text_command", json!({"command": format!("Document.CopyToDesktop {}", target)})); if r["success"].as_bool() != Some(true) { return json!({"success": false, "error": format!("Document.CopyToDesktop failed: {}", r["error"].as_str().unwrap_or("unknown")), "data": {"outputPath": out}}); }+ if let Some(s) = staged { if let Err(e) = crate::fusionpath::finish_output(&s, &out) { return json!({"success": false, "errorCode": "stage_failed", "error": e, "data": {"outputPath": out, "stagedAt": s}}); } } let mut result = match std::fs::metadata(&out) { Ok(m) => json!({"success": true, "output": format!("Saved library to {} ({} bytes)", out.rsplit('/').next().unwrap_or(&out), m.len()), "data": {"outputPath": out, "fileSize": m.len()}}), Err(_) => json!({"success": true, "output": "Document.CopyToDesktop executed (file may still be writing)", "data": {"outputPath": out}}) }; let (shots, _) = crate::lifecycle::post_open_screenshot(ctx, 1.0); result["data"]["postOpenScreenshot"] = shots;
rust/bridge/src/testproject.rs+4−2@@ -117,7 +117,8 @@ pub fn test_project_activate(ctx: &Ctx, kind: &str, pass: &dyn Fn(&str, Value) - let Some(tp) = crate::small_verbs::get_pref(&ctx.state_dir, "testProject") else { return false }; let Some(path) = tp[kind].as_str().filter(|p| !p.is_empty()) else { return false }; let base = tp["docBase"].as_str().unwrap_or("").to_lowercase();- let _ = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", ctx.host_paths_short(path))}));+ let fp = crate::fusionpath::for_fusion(&ctx.state_dir, path).unwrap_or_else(|_| path.replace('\\', "/"));+ let _ = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", fp)})); for _ in 0..6 { std::thread::sleep(std::time::Duration::from_secs(2)); if !base.is_empty() && pass("get_app_state", json!({}))["data"]["activeDocument"].as_str().unwrap_or("").to_lowercase().contains(&base) { return true; }@@ -150,7 +151,8 @@ pub fn make_test_project(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> let _ = std::fs::write(&sch_path, &sch); let _ = std::fs::write(&brd_path, &brd); let mut steps: Vec<String> = Vec::new(); for (label, fpath) in [("board", &brd_path), ("schematic", &sch_path)] {- let r = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", ctx.host_paths_short(fpath))}));+ let fp = crate::fusionpath::for_fusion(&ctx.state_dir, fpath).unwrap_or_else(|_| fpath.replace('\\', "/"));+ let r = pass("execute_text_command", json!({"command": format!("Document.newDesignFromLocal {}", fp)})); let mut okd = r["success"].as_bool() == Some(true); if !okd { let expected = fpath.rsplit('/').next().unwrap_or("").rsplit_once('.').map(|(s, _)| s.to_lowercase()).unwrap_or_default();
tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.sh