app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
0.9.20 source
17 files changed
+224−45
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+19−7bridge.json+1−1docs/ECOSYSTEM-TABLE.md+1−1docs/RENDERING.md+47−12install.shpackage.json+1−1references/modelling-and-export.md+6−2src/comhost/com.rs+29−10src/comhost/mock.rs+13−2src/render.rs+28−3src/visualize.rs+19−3tests/render.rs+48tests/visualize.rs+9uninstall.shBRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.19+0.9.20
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.9.19"+version = "0.9.20" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.9.19"+version = "0.9.20" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+19−7@@ -203,18 +203,20 @@ regardless, so "what CAD files do I have?" is answerable today. `swx_render_image {reason, width:3840, height:2160, view:"isometric", realView:true, ambientOcclusion:true, shadows:true, perspective:true,-zoomToFit:true, zoomFactor:2.0}` draws the active document with SOLIDWORKS's own+zoomToFit:true, zoomFactor:1.5}` draws the active document with SOLIDWORKS's own viewport (PhotoView 360 is absent on this edition) through `IModelDoc2::SaveBMP` at the size asked, converts it to PNG, and reads the size back from the file — `capped:true` if only the window size came out. MEASURED 2026-10-05 on the-110-document Moveo assembly with the window minimized: 3840×2160 in 11.6 s-(1080p in 10.4 s), a real RealView render with a floor shadow, every setting-read back restored. Every display setting it changes is captured first and+110-document Moveo assembly with the window minimized: 3840×2160 in ~11 s+(1080p in 10.4 s), shaded with a floor shadow and ambient occlusion, every+setting read back restored. RealView did not take on this laptop (uncertified+GPU): every set is read back, a set SOLIDWORKS ignored is `why:"setIgnored"`+(never "applied") and the reply's `notes` says so. `zoomFactor` >1 zooms in;+after a fit 1.4–1.6 fills the frame (2.0 clipped the tall Moveo). Every display setting it changes is captured first and restored after, on errors too; `settings` accounts for each one the caller asked for (`{requested, before, after, changed, restored, why}`); the document is never saved. A render that changes the screen waits for the user to be away. Still-unmeasured: a visible window, 8192 px, background changes, `zoomFactor`'s-direction. SOLIDWORKS Visualize can be started (`swx_start {app:"visualize"}`,+unmeasured: a visible window, 8192 px, background changes. SOLIDWORKS Visualize can be started (`swx_start {app:"visualize"}`, through its platform shortcut, its dialogs reported and never answered); a Visualize render is design-only. Details: [docs/RENDERING.md](docs/RENDERING.md). @@ -481,7 +483,17 @@ not. ## Roadmap -- v0.9.19 (this): **the render is measured; Visualize can be started.** The+- v0.9.20 (this): **a setting SOLIDWORKS ignores is not "applied".** The second+ live render (4K, 11.2 s, `zoomFactor:2.0`) showed RealView's put returning OK+ and the read-back staying false (uncertified RTX 4090 Laptop GPU); 0.9.19 still+ called it applied. Every set is now read back: ignored → `why:"setIgnored"`,+ `changed:false`, `restored:null`, plus a `notes[]` sentence; no machine-wide+ workaround is applied. `zoomFactor` measured (>1 zooms in; 2.0 clipped the+ Moveo — 1.4–1.6 after a fit). swconst ids recorded (AO 485, shadows 131 on+ 2026 SP3). `swx_status.visualize` was measured reading the real WPF first-run+ dialog; it now reports the top-level `hwnd` whenever Visualize has its main+ window — on first run the dialog IS that window. 553 tests.+- v0.9.19: **the render is measured; Visualize can be started.** The render ran live on 2026-10-05 (4K in 11.6 s, minimized window, every setting restored). It now accounts for every requested setting (`settings`: already as asked vs applied vs unreadable — the live run could not tell about
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.9.19",+ "version": "0.9.20", "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/ECOSYSTEM-TABLE.md+1−1@@ -114,7 +114,7 @@ templates**, alisamsam **22**. | 48 | **A title collision between SAVED documents is impossible here — measured, not assumed** | ✗ | **✓ MEASURED LIVE 2026-09-17** — `tools/drain-gate.py --fixture new` copied a bridge-created document's own file to a second directory under the same basename and called `swx_open` on it. SOLIDWORKS REFUSED: `errors = 65536` (`swFileWithSameTitleAlreadyOpen`). So the drain's title-collision guard — which refuses to close any document whose title another open document shares, and names both paths — can only ever fire for PATHLESS documents, which is exactly the state identity-at-birth removes. The guard stays (a document the bridge did not create can still be pathless, and an older bridge's leftovers are), and the measurement is recorded next to it in `com.rs` so nobody "simplifies" it on the assumption that saved collisions are the common case. No MCP server measures this at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 52 | **Never ends a session the user is working in** (a measured user-presence guard on every quit and sweep) | ✗ | **✓ 0.9.9, from an event MEASURED 2026-10-01**: Drew took over a bridge-launched session — a "leave as found" quit met his File Open dialog, and minutes later the session held his 34-document crate assembly. Every quit, sweep and priming now measures presence (SOLIDWORKS foreground + input under 120 s, documents the bridge did not open, an earlier take-over), refuses `user_is_active` with the evidence, fails closed on an unreadable measurement, and marks the session taken over for good; only the user's explicit override proceeds, and the gates never pass it. 0.9.10: a document is the bridge's only if a bridge verb opened or created it — in a session the user launched, every document is theirs. No MCP server has a presence guard at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 53 | **Signs the user in, unattended, without ever seeing the password** (3DEXPERIENCE ID sign-in from the user's own local vault) | ✗ | **✓ 0.9.13, from Drew's request 2026-10-02**: the login window's accessible inventory is captured every time it appears; `swx_sign_in` recognizes the form from that inventory alone, takes the login from the user's pup vault on their laptop through ab, types it through UI Automation, ticks "Remember me", verifies by the window closing, and scrubs the password from every reply, log and ledger line; it fails closed on a missing login, an unknown form or a second step, and never retries a rejected password. No MCP server signs in at all | ✗ | ✗ | ✗ | ✗ | ✗ |-| 54 | **Renders a product shot at any size, and leaves the user's view as it was** (high-resolution image export from the viewport) | ✗ | **✓ 0.9.18, MEASURED LIVE 2026-10-05** (3840×2160 of the 110-document Moveo assembly in 11.6 s, every setting restored; 0.9.19 accounts for every requested setting and adds `zoomFactor`): `swx_render_image` sets view, style, RealView, ambient occlusion, shadows, perspective and a plain background, exports through `IModelDoc2::SaveBMP` at up to 8192 px a side, converts to PNG and reads the size back from the file (`capped:true` when only the window size came out); every changed setting is captured, restored on every path, read back and listed; the swconst ids are read from the installed interop DLL, never guessed; a render that changes the screen respects the user-presence guard. The Fusion bridge has no render verb, and no MCP server renders at all | ✗ | ✗ | ✗ | ✗ | ✗ |+| 54 | **Renders a product shot at any size, and leaves the user's view as it was** (high-resolution image export from the viewport) | ✗ | **✓ 0.9.18, MEASURED LIVE 2026-10-05** (3840×2160 of the 110-document Moveo assembly in 11.6 s, every setting restored; 0.9.19 accounts for every requested setting and adds `zoomFactor`; 0.9.20 reads every set back, so a setting SOLIDWORKS ignores — RealView on an uncertified GPU, measured — is reported `setIgnored` with a note, never "applied"): `swx_render_image` sets view, style, RealView, ambient occlusion, shadows, perspective and a plain background, exports through `IModelDoc2::SaveBMP` at up to 8192 px a side, converts to PNG and reads the size back from the file (`capped:true` when only the window size came out); every changed setting is captured, restored on every path, read back and listed; the swconst ids are read from the installed interop DLL, never guessed; a render that changes the screen respects the user-presence guard. The Fusion bridge has no render verb, and no MCP server renders at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 51 | **Dialogs judged by MEASURED MODALITY, not by existence** (a notification never blocks; a modal always does) | ✗ | **✓ LIVE-PROVEN 2026-10-01 on 0.9.4** — with the 3DEXPERIENCE "Connected" notice on screen, `swx_stop` quit in 5.1 s and reported it as a notification with its evidence. **0.9.4, from a blocker MEASURED 2026-10-01**: the 3DEXPERIENCE "Connected to …" notice — a title-less `#32770` tool window whose owner frame stayed ENABLED for 95 s, with no Button-class control — blocked every quit, sweep and import for minutes. Now a window blocks when its kind is known-blocking, its owner frame is disabled, or it has a Button-class control; it is a `notification` (reported with owner, ownerEnabled, styles, rect and child controls; never answered) only when all of that was measured; unread evidence fails closed. Template placeholder captions are flagged, never presented as the window's words. **0.9.5 (adversary pass 43): the window's WORDS count — a save/recovery/import kind in its text blocks whatever the evidence, two or more button-like captions in any control class block, and measured notification evidence overrides a licence-type title alone; 0.9.7: swx_dismiss_blocking_dialogs uses the same title-and-words classifier before it closes anything; 0.9.8, from a live measurement: an owner frame read disabled for an instant (SOLIDWORKS busy) is re-read before a notice is called a modal.** No MCP server looks at a dialog at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 50 | **Readiness gating — attached is not ready** (no document command reaches a SOLIDWORKS that has not finished starting) | ✗ | **✓ 0.9.3, from a crash MEASURED 2026-10-01**: `swx_new_document` 8 s after COM attach, `swx_open` at 9 s and `swx_close_all_safely` at 12 s crashed SOLIDWORKS while its 3DEXPERIENCE add-in was still initialising (performance log: LOAD_ADDIN still running, SWEXH/UNHANDLED, the 3DEXPERIENCE marker only after the document commands, SWERR_REPORT); a control launch with no document commands crashed nothing. Now a READY stage follows `attached`, decided by two independent signals — COM `StartupProcessCompleted` and the performance log's 3DEXPERIENCE marker after the last LOAD_ADDIN — with an honest grace and timeout that never promote to ready; every verb that reaches SOLIDWORKS refuses `host_app_starting` with `retryAfterSeconds` until then — **0.9.5: gated by default with a pinned 16-verb exemption list, the performance log bound to this session's process creation time (an earlier session's log is never "complete"), COM alone never enough on a machine that writes markers, an on-demand add-in told from a start by quiet + which add-in, the grace unlocking reads only, and the window-message quit rungs ungated under userAuthorized; LIVE 2026-10-01 a document verb was refused during startup. 0.9.6: every gated reply carries the readiness its gate evaluated, the log is read from its per-start header, and the on-demand rule keys on the platform DLL SWK3DCompassAddin.dll; 0.9.7 (LIVE-PROVEN 0.9.6): no marker history — a disabled platform add-in never locks the bridge out, and a log still being written holds even a machine's first start; 0.9.11, from a live lock-out: `UNLOAD_ADDIN` is never read as a load, a session older than ten minutes with COM's start complete is never held by a log rule unless startup lines are still changing, and the ready latch survives a bridge restart**; a sweep during which SOLIDWORKS dies says `host_app_gone` with the log's crash markers. The rescue held under the crash (26,129 bytes verified). No MCP server distinguishes attached from ready at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 49 | **The user's rescued work survives the bridge's own updates — and ab's** (recovery never lives in a directory anything else owns) | ✗ | **✓ 0.9.1: the data root is `%LOCALAPPDATA%\adom-solidworks-bridge\`, a directory only this bridge writes to** — moved out of `Adom Bridge\` altogether after adversary pass 39 found 0.9.0's `Adom Bridge\solidworks-data` sitting inside the directory ab's installer and uninstaller own, whose fate on an ab uninstall or reinstall is documented nowhere; both earlier locations are migrated oldest first, every copy recorded by (root, path, size, mtime, sha256) so a legacy file that changes after its copy is copied again BESIDE it and reported `changedSinceCopy`, never settled by name — and since 0.9.2 a small file whose size and mtime match is hashed too, and two versions of the ledger or the ownership registry are MERGED (union, both versions kept beside) rather than one picked; a temp-directory fallback makes every sweep refuse (`data_root_is_temp`), and an unwritable recovery folder is refused once before the first save (`recovery_dir_unwritable`). **0.9.0, from a SEV-1 MEASURED 2026-09-30**: `bridges-cache\solidworks` was found CREATED 2026-09-18 09:42 by ab's auto-install of 0.8.9 — the updater re-extracts the whole directory, and every recovery folder from the day before, the ledger and the ownership registry went with it (test documents only; nothing of Drew's was lost). The data root is now `%LOCALAPPDATA%\Adom Bridge\solidworks-data\`, a sibling of the install cache; the first start MIGRATES what an older bridge left (copied under a temporary name and renamed into place once verified byte for byte — so a start that dies mid-copy never leaves a truncated rescue under the real name — never over an existing file, originals left in place, each file once, a copy that does not verify removed and retried next start); `swx_recover_list` still reads the old location and marks each rescue there `atRiskFromUpdate`, saying whether a verified copy exists — and in the data root calls a file whose copy has not landed `notCopiedYet` (pointing at its only copy), never "gone"; and a sweep REFUSES (`recovery_inside_install_dir`, nothing written, nothing closed) if its recovery folder would land under the install directory or the install cache — however the path is spelled (links, `..`, case). Every rule mutation-verified. No MCP server keeps a recovery folder at all | ✗ | ✗ | ✗ | ✗ | ✗ |
docs/RENDERING.md+47−12@@ -34,12 +34,15 @@ On adom_drew2, hosted 0.9.18, SOLIDWORKS 2026 SP3 (Makers), the Moveo assembly |---|---| | `{width:1920, height:1080}` | ok, 411,368 bytes, PNG header 1920×1080, `capped:false`, nothing applied or restored, 10,403 ms | | `{width:3840, height:2160, view:"isometric", displayStyle:"shaded", perspective:true, realView:true, ambientOcclusion:true, shadows:true, zoomToFit:true}` | ok, 979,997 bytes, PNG header 3840×2160, `capped:false`, 11,597 ms. Applied: ambientOcclusion false→true, displayMode 5→4, orientation isometric, perspective false→true, realView false→true, zoom fit. Restored: every one read back equal to its old value; camera captured and restored; `restoreFailures: []` |+| hosted 0.9.19, the same plus `zoomFactor:2.0` | ok, 932,312 bytes, 3840×2160, 11,157 ms, `restoreFailures: []`. `settings.shadows`: before true, `why:"alreadyAsRequested"` (the 0.9.18 gap explained: shadows were already on). `settings.realView`: before false, after **false** — the put returned OK and SOLIDWORKS left it off (0.9.19 still said `changed:true, why:"applied"`; 0.9.20 says `setIgnored`). `zoomFactor`: Scale2 0.176 → 0.352, the model twice as large — and the arm's tip clipped at the top edge. `enumIds`: `swDisplayAmbientOcclusionShadows: 485`, `swDisplayShadowsInShadedMode: 131` | - [measured] The image is a real render: RealView shading, a floor shadow, the scene's gradient background. - [measured] PowerShell reflection over the installed swconst DLL resolved all- four preference ids (no `render_enum_unresolved`). 0.9.19 adds `enumIds` to- the reply so the next run records the numbers themselves.+ four preference ids (no `render_enum_unresolved`). Measured on SOLIDWORKS 2026+ SP3 (0.9.19's `enumIds`): `swDisplayAmbientOcclusionShadows = 485`,+ `swDisplayShadowsInShadedMode = 131`. The bridge still reads them from the+ installed DLL every time; these are the record, not a hard-coded table. - [measured] SaveBMP at 4K works with the window MINIMIZED, and 4K costs only ~1.2 s more than 1080p — ask for 3840×2160 for hero-grade output. - [measured] The gap that led to `settings`: `shadows:true` appeared in neither@@ -48,10 +51,23 @@ On adom_drew2, hosted 0.9.18, SOLIDWORKS 2026 SP3 (Makers), the Moveo assembly - [measured] Framing: `zoomToFit` leaves this assembly at ~25% of the frame's width (the fit box is the whole assembly at the window's aspect, then exported 16:9). 0.9.19 adds `zoomFactor`.+- [measured] `zoomFactor` > 1 zooms IN: 2.0 took Scale2 0.176 → 0.352 and drew+ the model twice as large — and OVERSHOT on the Moveo (the arm's tip clipped at+ the top edge). After a fit, 1.4–1.6 usually fills the frame without clipping;+ 2.0 clipped a tall assembly.+- [measured] Shadows were already on in Drew's view (`alreadyAsRequested`).+- [measured] REALVIEW DID NOT TAKE on this laptop: `ViewDisplayRealView` put+ returned OK and the read-back stayed false. RealView needs a certified+ graphics card (this laptop has an RTX 4090 Laptop GPU, not on the certified+ list) and may also be unavailable while the window is minimized. The image was+ rendered shaded with shadows and ambient occlusion. Since 0.9.20 every set is+ read back: a set SOLIDWORKS ignored is `why:"setIgnored"`, `changed:false`,+ `restored:null`, absent from `appliedSettings`, and the reply's `notes[]`+ carries a sentence saying so. The bridge changes no machine-wide setting to+ force RealView on — that would be a standing change on the user's machine. - [unmeasured] flicker or resizing with the window VISIBLE; the largest size- (8192×4608); a `background` change (not passed in the live runs); the- direction of `zoomFactor` (a larger `Scale2` = closer?); whether shadows- were already on.+ (8192×4608); a `background` change (not passed in the live runs); RealView+ with the window visible. ### `settings` — every requested setting, accounted for (0.9.19) @@ -63,11 +79,20 @@ On adom_drew2, hosted 0.9.18, SOLIDWORKS 2026 SP3 (Makers), the Moveo assembly "after": <its value after applying, before the export>, "changed": true | false, "restored": true | false | null, // null when nothing was changed- "why": "applied" | "alreadyAsRequested" | "unreadable" | "setFailed" | "idUnresolved"+ "why": "applied" | "alreadyAsRequested" | "unreadable" | "setFailed" | "idUnresolved" | "setIgnored" }, …-}+},+"notes": [ "<one sentence per setting SOLIDWORKS ignored>" ] ``` +`setIgnored` (0.9.20): the set call returned OK but the read-back right after+was not the requested value — nothing changed, so nothing is restored+(`changed:false, restored:null`) and it is never listed as applied. RealView's+note is fixed text: "RealView was requested but SOLIDWORKS left it off — RealView+needs a certified graphics card (this laptop has an RTX 4090 Laptop GPU, not on+the certified list) and may also be unavailable while the window is minimized;+the image was rendered shaded with shadows and ambient occlusion."+ Names: `view`, `displayStyle`, `perspective`, `realView`, `ambientOcclusion`, `shadows`, `background`, `zoomToFit`, `zoomFactor` — one row for each one the caller asked for, and only those.@@ -149,11 +174,12 @@ shows the stage. ### Track A — what the 2026-10-05 run answered, and what is still open -Answered: 1 (4K honoured, 11.6 s, RealView/shadow/gradient visible), 2 (every-setting restored), 3 (all four ids read — numbers to be recorded from-`enumIds`), 4 (GetExecutablePath worked, by implication: the DLL was found).-Still open: 5 (visible-window flicker), 6 (8192×4608), a background change,-`zoomFactor`'s direction, and the AO setting's scope.+Answered: 1 (4K honoured, ~11 s, shadow/gradient visible; RealView did NOT take+— uncertified GPU, `setIgnored` since 0.9.20), 2 (every setting restored), 3+(ids read; 485 / 131 on 2026 SP3), 4 (GetExecutablePath worked, by implication:+the DLL was found), `zoomFactor`'s direction (>1 zooms in; 1.4–1.6 after a fit).+Still open: 5 (visible-window flicker), 6 (8192×4608), a background change, and+the AO setting's scope. ### Track A — the original measure-first list @@ -195,6 +221,15 @@ Still open: 5 (visible-window flicker), 6 (8192×4608), a background change, will be created to provide a place for this software to store data." — an edit prefilled `C:\Users\drew\Documents`, "…", "Continue". It creates a folder on the user's machine, a standing change: only the user answers it.+- [measured] the WPF dialog CAN be read: hosted 0.9.19's `swx_status.visualize`+ read it through the `desktop_ui_tree` fallback — `waitingOn:+ "userVisualizeContentFolder"`, its text, and `prefilledPath:+ "C:/Users/drew/Documents"`. UIA gives that path with FORWARD slashes (the+ dialog draws backslashes); the bridge reports it as UIA gave it and does not+ normalise. On first run the dialog IS the main window (same hwnd, class+ `HwndWrapper[SLDWORKSVisualize.exe;Main Application;…]`); 0.9.19 said+ `hwnd:null` beside `dialog.hwnd`, 0.9.20 reports the top-level `hwnd` whenever+ the process has that window, dialog or not. - [unmeasured] whether the shortcut accepts a model path; how to quit Visualize. **0.9.19:** `swx_start {app:"visualize", reason, waitSeconds?}` finds the
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.9.19",+ "version": "0.9.20", "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": [
references/modelling-and-export.md+6−2@@ -127,9 +127,13 @@ a verdict. - `settings` has a row for every setting you asked for: `changed:false` with `why:"alreadyAsRequested"` means it was already that way; `why:"unreadable"` or `"setFailed"` means it was NOT applied — say so rather than claiming it.+ `why:"setIgnored"` means SOLIDWORKS accepted the call and left the setting as+ it was (measured: RealView on an uncertified GPU); the reply's `notes` has a+ sentence for each — pass it on to the user. Never try to force RealView on. - If the model looks small after `zoomToFit` (an assembly fit leaves it at about- a quarter of a 16:9 frame), add `zoomFactor` (e.g. 2.0); the camera is- restored afterwards.+ a quarter of a 16:9 frame), add `zoomFactor`: >1 zooms in, and after a fit+ 1.4–1.6 usually fills the frame without clipping — 2.0 clipped a tall+ assembly. The camera is restored afterwards. - Every display setting it changed is restored (`restoredSettings`); if `restoreIncomplete` is true, tell the user which view settings may have changed. `background:"transparent"` is not available. A render that changes
src/comhost/com.rs+29−10@@ -4341,9 +4341,14 @@ impl ComBackend { if let Some(was) = was { if was != m { if view.put("DisplayMode", &Var::i4(m)).is_ok() {- undo.push(("displayMode", Was::Mode(was)));- applied.insert("displayMode".into(), json!({"from": was, "to": m}));- why = "applied";+ // A SET IS ONLY "APPLIED" IF IT READS BACK (0.9.20).+ if view.int("DisplayMode", &[]) == Some(m) {+ undo.push(("displayMode", Was::Mode(was)));+ applied.insert("displayMode".into(), json!({"from": was, "to": m}));+ why = "applied";+ } else {+ why = "setIgnored";+ } } else { why = "setFailed"; }@@ -4357,10 +4362,12 @@ impl ComBackend { if let Some(was) = was { if was != p { let r = view.call(if p { "AddPerspective" } else { "RemovePerspective" }, &[]);- if r.is_ok() {+ if r.is_ok() && view.flag("HasPerspective", &[]) == Some(p) { undo.push(("perspective", Was::Persp(was))); applied.insert("perspective".into(), json!({"from": was, "to": p})); why = "applied";+ } else if r.is_ok() {+ why = "setIgnored"; } else { steps_failed.push("perspective".into()); why = "setFailed";@@ -4375,9 +4382,17 @@ impl ComBackend { if let (Some(was), Some(ext)) = (was, ext.as_ref()) { if was != rv { if ext.put("ViewDisplayRealView", &Var::boolean(rv)).is_ok() {- undo.push(("realView", Was::Real(was)));- applied.insert("realView".into(), json!({"from": was, "to": rv}));- why = "applied";+ // MEASURED 2026-10-05: the put returned OK and the read-back+ // right after was still FALSE — RealView needs a certified+ // graphics card (and may be unavailable while minimized).+ // That is "setIgnored", never "applied".+ if ext.flag("ViewDisplayRealView", &[]) == Some(rv) {+ undo.push(("realView", Was::Real(was)));+ applied.insert("realView".into(), json!({"from": was, "to": rv}));+ why = "applied";+ } else {+ why = "setIgnored";+ } } else { why = "setFailed"; }@@ -4393,9 +4408,13 @@ impl ComBackend { if let (Some(was), Some(pid)) = (was, pid) { if was != v { if sw.call("SetUserPreferenceToggle", &[&Var::i4(pid), &Var::boolean(v)]).is_ok() {- undo.push((if key == "shadows" { "shadows" } else { "ambientOcclusion" }, Was::Toggle(pid, was)));- applied.insert(key.into(), json!({"from": was, "to": v}));- why = "applied";+ if Self::pref_toggle(&sw, pid) == Some(v) {+ undo.push((if key == "shadows" { "shadows" } else { "ambientOcclusion" }, Was::Toggle(pid, was)));+ applied.insert(key.into(), json!({"from": was, "to": v}));+ why = "applied";+ } else {+ why = "setIgnored";+ } } else { why = "setFailed"; }
src/comhost/mock.rs+13−2@@ -3144,8 +3144,11 @@ impl MockBackend { // `mock-render-unreadable-<key>`: that setting's read fails (the // measured `shadows` gap: requested, absent from applied). let unreadable = |k: &str| fault(&format!("mock-render-unreadable-{k}"));+ // `mock-render-ignores-<key>`: the set "succeeds" and SOLIDWORKS+ // leaves the value as it was (MEASURED 2026-10-05: RealView).+ let ignored = |k: &str| fault(&format!("mock-render-ignores-{k}")); let mut set = |k: &str, v: Value, applied: &mut serde_json::Map<String, Value>| {- if unreadable(k) {+ if unreadable(k) || ignored(k) { return; } if now.get(k) != Some(&v) {@@ -3237,7 +3240,15 @@ impl MockBackend { let b = if unreadable(key) { Value::Null } else { before.get(key).cloned().unwrap_or(Value::Null) }; let a = if unreadable(key) { Value::Null } else { after_apply.get(key).cloned().unwrap_or(Value::Null) }; let changed = !b.is_null() && a != b;- let why = if b.is_null() { "unreadable" } else if changed { "applied" } else { "alreadyAsRequested" };+ let why = if b.is_null() {+ "unreadable"+ } else if changed {+ "applied"+ } else if requested.is_boolean() && a != requested {+ "setIgnored"+ } else {+ "alreadyAsRequested"+ }; let restored_now = if changed { json!(after.get(key) == before.get(key)) } else { Value::Null }; settings.insert(name.into(), json!({"requested": requested, "before": b, "after": a, "changed": changed, "restored": restored_now, "why": why}));
src/render.rs+28−3@@ -126,7 +126,9 @@ pub fn request(args: &Value, doc_title: &str) -> Result<Request, Value> { // FRAMING (0.9.19): MEASURED 2026-10-05, zoomToFit leaves the Moveo // assembly at ~25% of a 16:9 frame (the fit box is the window's aspect). // `zoomFactor` multiplies IModelView::Scale2 AFTER any fit; the camera is- // restored as before. Its direction (>1 = closer) is UNMEASURED.+ // restored as before. MEASURED 2026-10-05: >1 zooms IN (Scale2 0.176 -> 0.352+ // at 2.0, the model twice as large) — and 2.0 clipped the tall Moveo arm;+ // 1.4–1.6 is the suggestion after a fit. let zoom_factor = match args.get("zoomFactor") { None | Some(Value::Null) => 1.0, Some(v) => match v.as_f64() {@@ -359,6 +361,28 @@ pub fn render_image(ctx: &crate::verbs::Ctx, args: &Value) -> Value { let capped = (pw as u64, ph as u64) != (req.width, req.height); let restore_failures = r.get("restoreFailures").cloned().unwrap_or(json!([])); let restore_incomplete = restore_failures.as_array().map(|a| !a.is_empty()).unwrap_or(false);+ // NOTES (0.9.20): a requested setting SOLIDWORKS ignored is said plainly.+ let settings = r.get("settings").cloned().unwrap_or(json!({}));+ let mut notes: Vec<String> = Vec::new();+ if let Some(m) = settings.as_object() {+ for (name, row) in m {+ if row.get("why").and_then(|w| w.as_str()) == Some("setIgnored") {+ notes.push(if name == "realView" {+ "RealView was requested but SOLIDWORKS left it off — RealView needs a certified graphics \+ card (this laptop has an RTX 4090 Laptop GPU, not on the certified list) and may also be \+ unavailable while the window is minimized; the image was rendered shaded with shadows and \+ ambient occlusion."+ .to_string()+ } else {+ format!(+ "{name} was requested ({}) but SOLIDWORKS left it at {} — the image was rendered without it.",+ row.get("requested").cloned().unwrap_or(Value::Null),+ row.get("after").cloned().unwrap_or(Value::Null)+ )+ });+ }+ }+ } crate::ledger::record( "render_image", json!({"ok": true, "path": req.png.to_string_lossy(), "width": pw, "height": ph, "capped": capped,@@ -377,7 +401,8 @@ pub fn render_image(ctx: &crate::verbs::Ctx, args: &Value) -> Value { "restoreFailures": restore_failures, // EVERY REQUESTED SETTING (0.9.19): {requested, before, after, changed, // restored, why} — "already as asked" is never confused with "not applied".- "settings": r.get("settings").cloned().unwrap_or(json!({})),+ "settings": settings,+ "notes": notes, // The swconst ids the host read from the installed interop DLL, for the record. "enumIds": r.get("enumIds").cloned().unwrap_or(Value::Null), // LOUD: a setting the render changed did not read back as restored.@@ -391,7 +416,7 @@ pub fn render_image(ctx: &crate::verbs::Ctx, args: &Value) -> Value { } else if capped { "The image came back SMALLER than asked (capped at the window's size) — width/height are what is actually in the file. Settings were restored; the document was not saved." } else {- "The image is at the requested size; width/height were read back from the PNG header. Every display setting the render changed was restored (restoredSettings; `settings` accounts for every one you asked for); the document was not saved. For hero-grade output ask for width:3840, height:2160 — measured 2026-10-05 it took only ~1.2 s longer than 1080p. If the model looks small after zoomToFit, add zoomFactor (e.g. 2.0)."+ "The image is at the requested size; width/height were read back from the PNG header. Every display setting the render changed was restored (restoredSettings; `settings` accounts for every one you asked for); the document was not saved. For hero-grade output ask for width:3840, height:2160 — measured 2026-10-05 it took only ~1.2 s longer than 1080p. zoomFactor >1 zooms in; after a fit, 1.4–1.6 usually fills the frame without clipping — 2.0 clipped a tall assembly. Read `notes`: a setting SOLIDWORKS ignored (RealView on an uncertified GPU) is listed there." }, })) }
src/visualize.rs+19−3@@ -21,6 +21,10 @@ //! creates a folder on the user's machine — a standing change — so the bridge //! NEVER answers it: `waitingOn:"userVisualizeContentFolder"`. //!+//! MEASURED 2026-10-05 (0.9.19): swx_status read the REAL WPF first-run dialog+//! through the desktop_ui_tree fallback — waitingOn userVisualizeContentFolder,+//! its text, prefilledPath "C:/Users/drew/Documents".+//! //! The bridge answers NONE of Visualize's dialogs. A 3DEXPERIENCE login goes to //! `swx_sign_in {userAuthorized:true, window:"visualize"}` on the user's //! say-so. Quitting Visualize is UNMEASURED and not built. Whether the shortcut@@ -67,7 +71,7 @@ pub fn classify(title: &str, class: &str, words: &str) -> Option<&'static str> { } fn is_main_window(class: &str) -> bool {- class.starts_with("HwndWrapper[SLDWORKSVisualize.exe;")+ class.to_ascii_lowercase().starts_with("hwndwrapper[sldworksvisualize.exe;") } /// A window's words: its Win32 child text and buttons; a WPF window has none, so@@ -91,6 +95,10 @@ fn words_of(os: &dyn crate::launchos::LaunchOs, hwnd: i64) -> (String, Option<St } let role = m.get("role").or_else(|| m.get("controlType")).and_then(|r| r.as_str()).unwrap_or(""); if role.eq_ignore_ascii_case("edit") && path.is_none() {+ // REPORTED AS UIA GAVE IT. MEASURED 2026-10-05: the+ // dialog SHOWS C:\Users\drew\Documents and UIA's value is+ // "C:/Users/drew/Documents" — forward slashes from the+ // page itself; the bridge does not normalise either way. *path = m.get("value").and_then(|v| v.as_str()).map(String::from); } m.values().for_each(|x| walk(x, names, path));@@ -119,10 +127,18 @@ pub fn status() -> Value { let windows = os.windows_of_pid(p.pid).unwrap_or_default(); let mut main: Option<i64> = None; let mut waiting: Option<Value> = None;- for w in windows.iter().filter(|w| w.visible) {- if is_main_window(&w.class_name) && main.is_none() {+ // THE MAIN WINDOW, dialog or not (0.9.20). MEASURED 2026-10-05: on first run+ // the content-folder dialog IS the main window (same hwnd, class+ // HwndWrapper[SLDWORKSVisualize.exe;Main Application;…]) and the block said+ // hwnd:null beside dialog.hwnd. Matched case-insensitively over EVERY window+ // of the process (visible or not), before any dialog classification.+ for w in windows.iter() {+ if is_main_window(&w.class_name) { main = Some(w.hwnd);+ break; }+ }+ for w in windows.iter().filter(|w| w.visible) { let (words, path) = words_of(os.as_ref(), w.hwnd); if let Some(kind) = classify(&w.title, &w.class_name, &words) { waiting = Some(match kind {
tests/render.rs+48@@ -242,3 +242,51 @@ fn every_requested_setting_says_what_happened_to_it() { let bad = b.call("swx_render_image", hero(json!({"zoomFactor": 0}))); check!(b, "a zoomFactor out of range: bad_zoom_factor", bad["errorCode"] == "bad_zoom_factor", s(&bad)); }++/// MEASURED 2026-10-05: RealView's put returned OK and the read-back was still+/// FALSE (uncertified GPU). A set SOLIDWORKS ignored is NOT "applied": why+/// setIgnored, changed false, restored null — and a `notes` line says so.+#[test]+fn a_setting_solidworks_ignores_is_not_reported_as_applied() {+ let scratch = TempDir::new("render-ignored");+ session(&scratch);+ std::fs::write(scratch.join("mock-render-ignores-realView"), "").unwrap();+ let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_render_image", hero(json!({})));+ let rv = &r["settings"]["realView"];+ check!(b, "the render itself still succeeded", r["success"] == true, s(&r));+ check!(+ b,+ "realView: why setIgnored, changed false, restored null, before=after=false",+ rv["why"] == "setIgnored" && rv["changed"] == false && rv["restored"].is_null()+ && rv["before"] == false && rv["after"] == false,+ s(rv)+ );+ check!(+ b,+ "not in appliedSettings, not in restoredSettings",+ r["appliedSettings"].get("realView").is_none() && r["restoredSettings"].get("realView").is_none(),+ s(&r)+ );+ let notes = r["notes"].as_array().cloned().unwrap_or_default();+ check!(+ b,+ "a notes line says RealView was requested but left off, and why",+ notes.iter().any(|n| {+ let n = n.as_str().unwrap_or("");+ n.starts_with("RealView was requested but SOLIDWORKS left it off") && n.contains("certified graphics card")+ && n.contains("minimized")+ }),+ s(&r["notes"])+ );+ check!(+ b,+ "the other settings were applied as usual",+ r["settings"]["shadows"]["why"] == "applied" && r["settings"]["ambientOcclusion"]["why"] == "applied",+ s(&r["settings"])+ );+ std::fs::remove_file(scratch.join("mock-render-ignores-realView")).unwrap();+ let clean = b.call("swx_render_image", hero(json!({})));+ check!(b, "with nothing ignored, notes is empty", clean["notes"] == json!([]), s(&clean["notes"]));+}
tests/visualize.rs+9@@ -97,6 +97,15 @@ fn the_first_run_content_folder_dialog_is_reported_and_never_answered() { ); let st = b.call("swx_status", json!({})); check!(b, "swx_status says the same", st["visualize"]["waitingOn"] == "userVisualizeContentFolder", s(&st["visualize"]));+ // MEASURED 2026-10-05: on first run the dialog IS the main window — the+ // block said hwnd:null beside dialog.hwnd. The top-level hwnd is reported+ // whenever the process has its HwndWrapper window, dialog or not.+ check!(+ b,+ "the top-level hwnd is the main window even while it shows the dialog (same hwnd)",+ st["visualize"]["hwnd"] == 9101 && st["visualize"]["dialog"]["hwnd"] == 9101 && st["visualize"]["ready"] == false,+ s(&st["visualize"])+ ); check!( b, "NOTHING was answered: no click, no text set, no keystroke",
uninstall.sh