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.
master
Drew Owens
The mechanical layer on 1.12.8 with the restart fixes (1.13.0-dev.1, add-in 1.0.23)
2ef0220
15d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
//! The OS boundary: two traits, nothing else.
use std::path::PathBuf;
/// What a running Fusion looks like to the bridge.
#[derive(Debug, Clone, serde::Serialize)]
pub struct FusionProcess {
pub pid: u32,
/// `Fusion360.exe` on Windows, `Autodesk Fusion` on macOS.
pub image: String,
pub parent_pid: Option<u32>,
}
#[derive(Debug, thiserror::Error)]
pub enum HostError {
#[error("Fusion is not installed on this machine: {0}")]
NotInstalled(String),
#[error("could not launch Fusion: {0}")]
Launch(String),
#[error("could not stop Fusion: {0}")]
Stop(String),
#[error("host operation failed: {0}")]
Other(String),
}
/// Process-shaped things only the OS can answer. Keep it small: if a method could be an
/// Adom Bridge verb (`process_list`, `process_kill`, window enumeration, screenshots,
/// clicks, UIA), it does NOT belong here.
pub trait HostProcess: Send + Sync {
/// The readiness POLL. Runs every few seconds for the life of the bridge, so it must be
/// microseconds in-process (Toolhelp32 snapshot on Windows; `sysctl`/`proc_listpids`
/// on macOS), never an HTTP round trip to ab.
fn fusion_processes(&self) -> Vec<FusionProcess>;
/// Launch Fusion detached from the bridge's own process tree so a bridge respawn never
/// takes Fusion down with it (CREATE_BREAKAWAY_FROM_JOB on Windows; `open -a` on macOS).
fn launch_fusion(&self) -> Result<FusionProcess, HostError>;
/// Whether the bridge runs elevated (drives the installer's --globalinstall decision).
fn is_elevated(&self) -> bool;
/// Is any process with one of these image names running (case-insensitive)? The Fusion
/// streamer / installer check on the readiness poll. In-process, like `fusion_processes`.
fn image_running(&self, _names: &[&str]) -> bool { false }
/// Tie a child (the Python sidecar during the port) to THIS process's lifetime, so a hard
/// kill of the bridge by ab (every bridge_install, every supervisor respawn) takes the
/// child with it instead of leaving an orphan on a random port. A Windows job object
/// with KILL_ON_JOB_CLOSE (and BREAKAWAY_OK, so the child's own Fusion launch can still
/// break away); a no-op elsewhere. Returns whether the tie took.
fn tie_child_lifetime(&self, _pid: u32) -> bool { false }
/// Guard the next `seconds` against Fusion-family focus theft (the background envelope:
/// foreground-lock timeout raised, minimize animation off, a watcher that pushes any
/// Fusion window that grabs the foreground back down unless the user is watching).
/// Fire-and-forget; a no-op off Windows.
fn background_envelope(&self, _seconds: f64, _minimize: bool) {}
/// Is the user watching Fusion (they brought it forward themselves during an envelope)?
fn user_watching(&self) -> bool { false }
/// The OS's own hang verdict for a top-level window (IsHungAppWindow); false off Windows.
fn window_hung(&self, _hwnd: i64) -> bool { false }
/// Summed kernel+user CPU ticks (100ns units) of the Fusion family; 0 when unreadable.
fn family_cpu_ticks(&self) -> u64 { 0 }
/// Physical px per DIP for a window (GetDpiForWindow / 96 on Windows). The same ribbon
/// control measured 296 DIP on a 4K box at 2.0 and a laptop at 1.5; rect fractions do not port.
fn window_dpi_scale(&self, _hwnd: i64) -> f64 { 1.0 }
/// Screen px of the window's CLIENT (0,0). Fusion draws its own title bar inside the client
/// area and a maximized window's rect overhangs the screen, so rect.left/top is 13 px off.
fn client_origin(&self, _hwnd: i64) -> (i32, i32) { (0, 0) }
/// The work area (left, top, width, height) of the monitor a window sits on, and whether
/// the window is maximized; None when unreadable. Used to grow a squint-sized Fusion window.
fn window_work_area(&self, _hwnd: i64) -> Option<(i32, i32, i32, i32)> { None }
fn window_is_maximized(&self, _hwnd: i64) -> bool { false }
}
/// Where things live on this OS. Every path the bridge ever touches comes from here.
pub trait HostPaths: Send + Sync {
/// The Fusion launcher/executable, if installed (`webdeploy/production/<hash>/FusionLauncher.exe`
/// on Windows; `/Applications/Autodesk Fusion.app` on macOS).
fn fusion_executable(&self) -> Option<PathBuf>;
/// Every add-in directory Fusion scans, newest convention first. Fusion has MOVED this
/// across versions; the installer writes to all of them.
fn addin_dirs(&self) -> Vec<PathBuf>;
/// Fusion's own application log (`AppLogFile<stamp>.log`), the only place its CEF alert
/// toasts are visible.
fn fusion_log_dir(&self) -> Option<PathBuf>;
/// The bridge's persistent state dir (`~/.adom/fusion-bridge`).
fn bridge_state_dir(&self) -> PathBuf;
/// A scratch dir for staged libraries, STEPs and screenshots.
fn scratch_dir(&self) -> PathBuf;
/// The mechanical operation journal and its dispatch handoff files, shared with the add-in
/// (`%LOCALAPPDATA%/Adom/FusionBridge/mechanical` on Windows, where dev builds left history).
fn mechanical_state_dir(&self) -> PathBuf;
/// A fresh install is streaming: some webdeploy dir has the launcher but no complete
/// Fusion360.exe exists anywhere yet (disk state, process-independent).
fn fusion_install_streaming(&self) -> bool { false }
/// Fusion is applying an auto-update: two or more launcher builds and one of them
/// touched in the last ~20 minutes (stale leftover builds never trigger it).
fn fusion_update_in_progress(&self) -> bool { false }
/// A path that survives Fusion's space-delimited executeTextCommand (8.3 short form on
/// Windows when the path has a space); the path itself elsewhere.
fn short_path(&self, path: &str) -> String { path.replace('\\', "/") }
/// `_clean_incomplete_webdeploy`: after a failed streamed launch, remove the partial
/// launcher-bearing webdeploy dirs - and NOTHING when any complete install exists (the
/// launcher-only stub beside a healthy app is legitimate). Returns the dir names removed.
fn clean_incomplete_webdeploy(&self) -> Vec<String> { vec![] }
}