app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
0.10.1 source
12 files changed
+211−16
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.10.0+0.10.1
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.10.0"+version = "0.10.1" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.0"+version = "0.10.1" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+15−1@@ -492,7 +492,21 @@ not. ## Roadmap -- v0.10.0 (this): **issue #1, the modelling DSL Michael asked for, and+- v0.10.1 (this): **the self-test never reports a closed SOLIDWORKS as a failure,+ and a Connected install is not called classic.** Measured on hosted 0.10.0+ with SOLIDWORKS closed: `swx_selftest` counted `swx_docs` and+ `swx_get_preferences` as failures ("SOLIDWORKS is not running"). Now:+ - every check that needs SOLIDWORKS is skipped, with the reason, and the+ summary says `solidworksRunning: false`;+ - the hint's first sentence asks for SOLIDWORKS to be started;+ - `level:"write"` is refused up front and changes nothing.++ `swx_status.solidworks.classicYears` said `[2026]` on the 3DEXPERIENCE 2026+ install. `registryYears` now carries the raw registry years, and+ `classicYears` only the years with no 3DEXPERIENCE entry or install path for+ that release, which is `[]` on that machine. The edition decision is+ unchanged. 576 tests.+- v0.10.0: **issue #1, the modelling DSL Michael asked for, and SOLIDWORKS 2021 as a supported release.** - **Sketches.** One `sketch` holds several profiles, so concentric circles are a ring. A sketch can go on an offset plane
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.10.0",+ "version": "0.10.1", "description": "Bridge for SOLIDWORKS desktop CAD — create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.", "author": "Adom Inc.", "license": "MIT",
docs/COMPATIBILITY.md+12−1@@ -40,7 +40,7 @@ only ever **run** against **3DEXPERIENCE SOLIDWORKS 2026** (Makers, revision | | 3DEXPERIENCE SOLIDWORKS (2026, measured) | Classic desktop (2021, unmeasured) | |---|---|---|-| How it is detected | An install path or registry entry naming 3DEXPERIENCE, or the 3DEXPERIENCE Launcher | An install path under `SOLIDWORKS Corp`, OR a registry key `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS <year>` with an exe found and no 3DEXPERIENCE marker. The exe is also looked up from `...\SOLIDWORKS <year>\Setup` value `SolidWorks Folder` [unmeasured]. `swx_status.solidworks.classicYears` lists the years. |+| How it is detected | An install path or registry entry naming 3DEXPERIENCE, or the 3DEXPERIENCE Launcher | An install path under `SOLIDWORKS Corp`, OR a registry key `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS <year>` with an exe found and no 3DEXPERIENCE marker. The exe is also looked up from `...\SOLIDWORKS <year>\Setup` value `SolidWorks Folder` [unmeasured]. `swx_status.solidworks.registryYears` lists every `SOLIDWORKS <year>` key. That is a fact about the registry, not the edition: the 3DEXPERIENCE 2026 install writes `SOLIDWORKS 2026` too (measured 2026-10-13). `classicYears` lists only the years with no 3DEXPERIENCE entry or install path for the same release; on Drew's 2026 machine that is `[]`. | | `edition` / `launchRoute` | `connected` / `platformShortcut` | `desktop` / `com` | | Launch | The Platform's own shortcut (CATSTART, then SWXDesktopLauncher), never the exe | `CoCreateInstance("SldWorks.Application")`, which starts `SLDWORKS.exe` itself. With several releases installed, set `SWX_PROGID=SldWorks.Application.29` to pick 2021 [unmeasured]. | | Sign-in | 3DEXPERIENCE ID (three measured hosts) | None. The licence is standalone or SolidNetWork; the bridge never sees a licence prompt and never answers one. |@@ -258,6 +258,17 @@ Each row is `{name, verb, pass, ms, errorCode, error, hostError}`. The the normal dispatcher, readiness gate included. The self-test is itself exempt from the gate, so a check held at the gate is reported, not hidden. +**Run it with SOLIDWORKS started.** With SOLIDWORKS closed (measured+2026-10-13), only `swx_status` and `swx_readiness` can run:+- every check that needs SOLIDWORKS is `skipped`, with the reason, and is never+ counted as a failure;+- the summary carries `solidworksRunning: false`;+- the `_hint` begins "Start SOLIDWORKS (swx_start, or by hand) and run this+ again".++`level:"write"` with SOLIDWORKS closed is refused up front+(`solidworks_not_running`) and changes nothing.+ ## What is still unmeasured on 2021 - The classic launch route (`CoCreateInstance`) and the `Setup\SolidWorks
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.10.0",+ "version": "0.10.1", "type": "app", "description": "Skills for your container so your AI knows how to drive the SOLIDWORKS bridge (swx_* verbs). The bridge runtime itself is the release zip — a single native swx-bridge.exe; Adom Bridge loads that.", "tags": [
src/detect.rs+78−7@@ -46,7 +46,8 @@ impl Detection { // CLASSIC DESKTOP RELEASES in the registry (0.10.0): SOLIDWORKS 2021 // is major version 29. Launched through COM (the exe), with no // 3DEXPERIENCE sign-in; see docs/COMPATIBILITY.md.- "classicYears": classic_years(&self.registry_versions),+ "registryYears": registry_years(&self.registry_versions),+ "classicYears": classic_years_from(self.exe.as_deref(), &self.registry_versions, edition), }) } @@ -119,7 +120,7 @@ pub fn edition_from( // `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS 2021`, a year and nothing else, with no // 3DEXPERIENCE marker anywhere (checked above) and an exe found. UNMEASURED // on a classic machine; it is the documented layout of the desktop installer.- if exe.is_some() && !classic_years(registry_versions).is_empty() {+ if exe.is_some() && !registry_years(registry_versions).is_empty() { return "desktop"; } if exe.is_none() {@@ -131,9 +132,11 @@ pub fn edition_from( "unknown" } -/// The classic desktop releases named in the registry: "SOLIDWORKS 2021" ->-/// 2021. A 3DEXPERIENCE entry ("SOLIDWORKS 3DEXPERIENCE R2026x") is not one.-pub fn classic_years(registry_versions: &[String]) -> Vec<u32> {+/// Every release year a plain `SOLIDWORKS <year>` registry key names:+/// "SOLIDWORKS 2021" -> 2021. A FACT about the registry, not a claim about the+/// edition — MEASURED 2026-10-13: the 3DEXPERIENCE (Connected) 2026 install+/// writes "SOLIDWORKS 2026" too, so this alone is not "a classic install".+pub fn registry_years(registry_versions: &[String]) -> Vec<u32> { let mut out: Vec<u32> = registry_versions .iter() .filter_map(|v| {@@ -147,6 +150,63 @@ pub fn classic_years(registry_versions: &[String]) -> Vec<u32> { out } +/// The release years tied to 3DEXPERIENCE / Connected: a registry entry or an+/// install path that names the platform and a year ("SOLIDWORKS 3DEXPERIENCE+/// R2026x" -> 2026).+pub fn connected_years(exe: Option<&str>, registry_versions: &[String]) -> Vec<u32> {+ fn years_in(s: &str) -> Vec<u32> {+ let b = s.as_bytes();+ let mut out = Vec::new();+ let mut i = 0;+ while i + 4 <= b.len() {+ let run = &b[i..i + 4];+ let boundary_before = i == 0 || !b[i - 1].is_ascii_digit();+ let boundary_after = i + 4 == b.len() || !b[i + 4].is_ascii_digit();+ if run.iter().all(|c| c.is_ascii_digit()) && boundary_before && boundary_after {+ if let Ok(y) = std::str::from_utf8(run).unwrap_or("").parse::<u32>() {+ if (2000..2100).contains(&y) {+ out.push(y);+ }+ }+ i += 4;+ } else {+ i += 1;+ }+ }+ out+ }+ let marked = |t: &str| {+ let u = t.to_uppercase();+ u.contains("3DEXPERIENCE") || u.contains("CONNECTED")+ };+ let mut out: Vec<u32> = registry_versions.iter().filter(|v| marked(v)).flat_map(|v| years_in(v)).collect();+ if let Some(e) = exe.filter(|e| marked(e)) {+ out.extend(years_in(e));+ }+ out.sort();+ out.dedup();+ out+}++/// The CLASSIC desktop releases on this machine: registry years with no+/// 3DEXPERIENCE entry or install path for the same release. On a Connected+/// machine whose platform marker names no year (the launcher alone), no year+/// can be shown to be classic, so none is claimed. MEASURED 2026-10-13: 0.10.0+/// reported `classicYears: [2026]` on the 3DEXPERIENCE 2026 install, which+/// read as a classic install that is not there. The EDITION decision is not+/// made here and did not change.+pub fn classic_years_from(exe: Option<&str>, registry_versions: &[String], edition: &str) -> Vec<u32> {+ let all = registry_years(registry_versions);+ if edition == "desktop" {+ return all;+ }+ let connected = connected_years(exe, registry_versions);+ if edition == "connected" && connected.is_empty() {+ return Vec::new();+ }+ all.into_iter().filter(|y| !connected.contains(y)).collect()+}+ /// How this bridge may start SOLIDWORKS. /// /// LIVE FINDING (2026-09-03) — THE LAUNCH ANSWER. The earlier conclusion that@@ -541,7 +601,7 @@ mod windows_impl { /// <year>\Setup` value "SolidWorks Folder" (newest year first). UNMEASURED. fn classic_setup_exe(versions: &[String]) -> Option<String> { let hive = RegKey::predef(HKEY_LOCAL_MACHINE);- let mut years = super::classic_years(versions);+ let mut years = super::classic_years_from(None, versions, "unknown"); years.reverse(); for y in years { if let Ok(key) = hive.open_subkey(format!(r"SOFTWARE\SolidWorks\SOLIDWORKS {y}\Setup")) {@@ -725,7 +785,18 @@ mod tests { assert_eq!(edition_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, false), "desktop"); assert_eq!(edition_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, true), "connected"); assert_eq!(edition_from(None, &y2021, false), "unknown", "no exe: nothing to launch");- assert_eq!(classic_years(&["SOLIDWORKS 2021".into(), "SOLIDWORKS 3DEXPERIENCE R2026x".into(), "SOLIDWORKS 2019".into()]), vec![2019, 2021]);+ assert_eq!(registry_years(&["SOLIDWORKS 2021".into(), "SOLIDWORKS 3DEXPERIENCE R2026x".into(), "SOLIDWORKS 2019".into()]), vec![2019, 2021]);+ // classicYears (0.10.1): MEASURED 2026-10-13, the 3DEXPERIENCE 2026 install+ // also writes "SOLIDWORKS 2026" — that year is not a classic install.+ let x2026 = r"C:\Program Files\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE R2026x\SOLIDWORKS\SLDWORKS.exe";+ let reg26 = vec!["SOLIDWORKS 2026".to_string()];+ assert_eq!(edition_from(Some(x2026), ®26, true), "connected", "the edition decision is unchanged");+ assert_eq!(registry_years(®26), vec![2026]);+ assert!(classic_years_from(Some(x2026), ®26, "connected").is_empty(), "the Connected release is not classic");+ assert!(classic_years_from(None, ®26, "connected").is_empty(), "a marker naming no year: no year can be shown to be classic");+ let mixed = vec!["SOLIDWORKS 2021".to_string(), "SOLIDWORKS 2026".to_string(), "SOLIDWORKS 3DEXPERIENCE R2026x".to_string()];+ assert_eq!(classic_years_from(None, &mixed, "connected"), vec![2021], "a classic 2021 beside a Connected 2026");+ assert_eq!(classic_years_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, "desktop"), vec![2021]); assert_eq!(edition_from(None, &none, false), "unknown"); }
src/selftest.rs+59−2@@ -84,12 +84,56 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { "edition": sw.get("edition"), "launchRoute": sw.get("launchRoute"), "versionSupport": sw.get("versionSupport"),+ "registryYears": sw.get("registryYears"), "classicYears": sw.get("classicYears"), "registryVersions": sw.get("registryVersions"), })); let readiness = run.call(ctx, "readiness", "swx_readiness", json!({})); let ready = readiness.get("ready").and_then(|v| v.as_bool()) == Some(true) || readiness.pointer("/solidworks/ready").and_then(|v| v.as_bool()) == Some(true);+ // IS SOLIDWORKS THERE TO TEST? MEASURED 2026-10-13 (hosted 0.10.0, SOLIDWORKS+ // closed): swx_docs and swx_get_preferences were reported as FAILURES with+ // "SOLIDWORKS is not running" — not a verb failing on this release, and a+ // user who ran the self-test before starting SOLIDWORKS would have sent us+ // false failures. So: every check that needs SOLIDWORKS is SKIPPED (why+ // said) when COM is not attached; status and readiness still run. A COM+ // host busy with a long verb is "cannot tell", and the checks run.+ let com = status.get("com").cloned().unwrap_or(Value::Null);+ let running = com.get("running").and_then(|v| v.as_bool()).or_else(|| com.get("attached").and_then(|v| v.as_bool()))+ .unwrap_or(com.get("busy").and_then(|v| v.as_bool()) == Some(true));+ if !running {+ let process = sw.get("running").and_then(|v| v.as_bool()) == Some(true);+ let why = if process {+ "SOLIDWORKS is running but this bridge is not attached to it yet (still starting, or waiting on a sign-in)"+ } else {+ "SOLIDWORKS is not running"+ };+ let ran = run.results.len();+ let start_hint = format!(+ "Start SOLIDWORKS (swx_start, or by hand) and run this again — with it closed only {ran} checks can run."+ );+ if level == "write" {+ // Refused UP FRONT: nothing was created or changed.+ let mut out = crate::verbs::err_code(+ "solidworks_not_running",+ format!("swx_selftest level:\"write\" needs SOLIDWORKS: {why}. Nothing was created or changed."),+ start_hint,+ );+ if let Value::Object(m) = &mut out {+ m.insert("results".into(), Value::Array(run.results));+ m.insert("solidworksRunning".into(), json!(false));+ }+ return out;+ }+ for (name, verb) in [+ ("document list", "swx_docs"),+ ("preferences", "swx_get_preferences"),+ ("the active document's read verbs", "swx_design_info"),+ ] {+ run.skip(name, verb, why);+ }+ return finish(level, &sw, ready, false, started, run, Some(start_hint));+ } let docs = run.call(ctx, "document list", "swx_docs", json!({})); let active = docs.pointer("/active/title").and_then(|t| t.as_str()).map(String::from); let active_type = docs.pointer("/active/type").and_then(|t| t.as_str()).map(String::from);@@ -210,9 +254,20 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { } } + finish(level, &sw, ready, true, started, run, None)+}++/// The reply: the rows, the counts, and — when SOLIDWORKS was not there —+/// that FIRST, so nobody reads a skipped check as a failure.+fn finish(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>) -> Value { let passed = run.results.iter().filter(|r| r["pass"] == true).count(); let failed = run.results.iter().filter(|r| r["pass"] == false).count(); let skipped = run.results.iter().filter(|r| r["pass"].is_null()).count();+ let send = "Send this WHOLE reply back (for example on the bridge's wiki issue): each row is one verb or one modelling op on your install, with the error SOLIDWORKS gave when it failed. level:\"read\" changed nothing; level:\"write\" built, exported, saved and closed a scratch part in the bridge's own scratch folder and re-activated the document that was active before.";+ let hint = match lead {+ Some(first) => format!("{first} {send}"),+ None => send.to_string(),+ }; crate::verbs::ok(json!({ "level": level, "bridgeVersion": crate::BRIDGE_VERSION,@@ -220,11 +275,13 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { "versionSupport": sw.get("versionSupport"), "edition": sw.get("edition"), "launchRoute": sw.get("launchRoute"),+ "registryYears": sw.get("registryYears"), "classicYears": sw.get("classicYears"), "ready": ready,+ "running": running, },- "summary": {"passed": passed, "failed": failed, "skipped": skipped, "seconds": started.elapsed().as_secs_f64()},+ "summary": {"passed": passed, "failed": failed, "skipped": skipped, "solidworksRunning": running, "seconds": started.elapsed().as_secs_f64()}, "results": run.results,- "_hint": "Send this WHOLE reply back (for example on the bridge's wiki issue): each row is one verb or one modelling op on your install, with the error SOLIDWORKS gave when it failed. level:\"read\" changed nothing; level:\"write\" built, exported, saved and closed a scratch part in the bridge's own scratch folder and re-activated the document that was active before.",+ "_hint": hint, })) }
tests/issue1.rs+42@@ -217,3 +217,45 @@ fn selftest_reports_every_check_and_cleans_up_after_itself() { let bad = b.call("swx_selftest", json!({"level": "everything"})); check!(b, "an unknown level is refused", bad["errorCode"] == "bad_level", s(&bad)); }++/// MEASURED 2026-10-13 (hosted 0.10.0, SOLIDWORKS closed): the self-test+/// reported swx_docs and swx_get_preferences as FAILURES ("SOLIDWORKS is not+/// running") — false failures a user would have sent back.+#[test]+fn selftest_with_solidworks_closed_skips_instead_of_failing_and_write_refuses() {+ let (b, _m) = boot("issue1-selftest-closed");+ let r = b.call("swx_selftest", json!({}));+ let rows = r["results"].as_array().cloned().unwrap_or_default();+ let skipped_why = |verb: &str| rows.iter().any(|x| x["verb"] == verb && x["pass"].is_null() && x["skipped"] == "SOLIDWORKS is not running");+ check!(+ b,+ "read level, SOLIDWORKS closed: nothing FAILED; docs/preferences/active-document reads are SKIPPED, why said; solidworksRunning:false",+ r["success"] == true && r["summary"]["failed"] == 0 && r["summary"]["solidworksRunning"] == false+ && r["summary"]["passed"] == 2 && skipped_why("swx_docs") && skipped_why("swx_get_preferences") && skipped_why("swx_design_info"),+ s(&r)+ );+ check!(+ b,+ "the hint's FIRST sentence says to start SOLIDWORKS and how many checks could run",+ r["_hint"].as_str().unwrap_or("").starts_with("Start SOLIDWORKS (swx_start, or by hand) and run this again — with it closed only 2 checks can run."),+ s(&r["_hint"])+ );+ let w = b.call("swx_selftest", json!({"level": "write"}));+ check!(+ b,+ "write level, SOLIDWORKS closed: refused UP FRONT with the same advice",+ w["success"] == false && w["errorCode"] == "solidworks_not_running"+ && w["_hint"].as_str().unwrap_or("").starts_with("Start SOLIDWORKS") && w["solidworksRunning"] == false,+ s(&w)+ );+ b.call("swx_start", json!({}));+ let docs = b.call("swx_docs", json!({}));+ let ledger = b.call("swx_action_ledger", json!({"limit": 200}));+ check!(+ b,+ "...and changed nothing: no document was created",+ docs["count"] == 0+ && !ledger["actions"].as_array().map(|a| a.iter().any(|e| e["action"] == "document_created")).unwrap_or(false),+ format!("{}\n{}", s(&docs), s(&ledger))+ );+}
uninstall.sh