merge origin/main: unify our web branch with 277 upstream commits
Conflicts resolved as unions of behavior wherever possible: - #1350 (set_properties numeric CAS): kept our stricter json_values_match (guards u64>f64 precision loss); upstream's duplicate removed. - #1351 (web open-from-bytes): our implementation wins (guarded queue, settle semantics, protocol 2) using upstream's validation helper so both sides' tests pass. - #1358/#1359 (Windows printer routing/media): merged both engines — upstream's route planning, media tables, GDI raster path and driver options on top of our level-2 enumeration fallback, true-default resolution, quoted printto params, existence probe, and error descriptions. - #1360 (plot-style leniency): decompress_ctb now carries both our CTBVER1/STBVER1 container tags and structured flag, and upstream's lenient reader with Windows-1252 handling. - 3D snapping precedence: upstream's F4 3D osnap system takes precedence over our #1030 solid edge wires when the mesh cache has geometry edges; our wires remain the fallback for triangle-only caches and mesh entities (both test suites pass). - Fixed upstream-side bugs surfaced by the merge: parametric arc grips impl/test contradiction, psetupin test path separators, and our pdf_raster regression where the new dpi argument was threaded but ignored (GDI 300-DPI would have cached at 150). - ocs_save repinned to the upstream acadrust rev; docs/command- reference.md regenerated (the staleness test caught it).
No files matched your search
@@ -17,6 +17,10 @@ The server provides four tools:
|
||||
- `ocs_execute` performs one operation, an atomic record update, or a sequential batch against the real editor.
|
||||
- `ocs_capture` returns a bounded PNG of the drawing viewport or complete window.
|
||||
|
||||
Native builds also accept a second, headless entry point (`OpenCADStudio --serve`, one
|
||||
JSON request per line) for scripts and CI; the two native channels, their startup dialogs
|
||||
and their limits are described in [native.md](native.md).
|
||||
|
||||
The normal flow is to call `ocs_sessions`, pass its returned `session_id` as `ocs_session_id` to the other tools, read the chosen session with `ocs_read`, then pass the returned document state into `ocs_execute`. For example, an undo request has this shape:
|
||||
|
||||
```json
|
||||
@@ -138,7 +142,27 @@ Execute responses use `response_detail: "compact"` by default and return only th
|
||||
|
||||
For unfamiliar or conditional commands, use `start`, then inspect `state.command` in every response. Its `accepts` array gives the valid MCP input kinds, `options` gives the current tokens, and `input_example` gives the next request shape. Add the current state fields and a new `request_id` to each step.
|
||||
|
||||
Every `ocs_execute` request requires a caller-generated `request_id`. Reuse that ID only to retry the identical request after a timeout, together with the same session ID, document ID, expected revision, and selection. Commands report `waiting_input` while more input is required and asynchronous work remains `running` until its real callback finishes. Geometry stays in OpenCADStudio and its geometry kernel.
|
||||
Every `ocs_execute` request requires a caller-generated `request_id`. Reuse that ID only to retry the identical request after a timeout, together with the same session ID, document ID, expected revision, and selection. Commands report `waiting_input` while more input is required and asynchronous work remains `running` until its real callback finishes.
|
||||
|
||||
A `run` reply also carries two fields that describe what happened to the line:
|
||||
|
||||
* `blocked_by` — `null` when the command finished, otherwise what *this line*
|
||||
left open and waiting: `"command"` (an interactive command), `"text_editor"`
|
||||
(the in-place editor opened by the `TEXT` content step), `"mtext_editor"`, or
|
||||
`"modal:<Kind>"` for a dialog. A surface that was already open when the line
|
||||
started (an earlier line's editor, another tab, a startup dialog) is not
|
||||
attributed to this line, so a caller polling for `completed` is never stuck
|
||||
waiting on someone else's editor. `blocked_by` is set whenever `status` is
|
||||
`waiting_input`.
|
||||
* `unconsumed` — the tokens of the line that no prompt ever asked for, in order.
|
||||
A fully consumed line reports `[]`; `CIRCLE 0,0 5 9` reports `["9"]` because
|
||||
the radius step ends the command. Over-typed input is reported instead of
|
||||
being dropped on the floor.
|
||||
|
||||
When a line ends at the `TEXT` content step (`TEXT 0,0 5 0 hello`) the text is
|
||||
typed into the in-place editor and committed, so the line behaves exactly as the
|
||||
same input typed into the GUI command line would: one `Text` entity, nothing
|
||||
unconsumed, and the command closed rather than left armed for a second line. Geometry stays in OpenCADStudio and its geometry kernel.
|
||||
|
||||
`ocs_capture` defaults to the drawing viewport and a longest edge of 1600 pixels. Set `scope` to `window` for the full interface or change `max_dimension` between 256 and 4096.
|
||||
|
||||
@@ -198,3 +222,4 @@ The operation stays pending until the open settles; if another open holds the sl
|
||||
### Startup prompts during automation
|
||||
|
||||
The first control request of a session marks it automated: a donation prompt that has not been shown yet is dropped, and no new one is queued while control stays enabled. A prompt already on screen is left for its reader — close it with `{"op":"action","name":"close_modal"}`. A web host that intends to automate from the first page load can set `localStorage["opencadstudio.automation"] = "1"` before loading the app so the prompt never appears (the same pre-load mechanism the settings use).
|
||||
The web build is driven through two JavaScript functions instead of a process; the protocol details are summarised above and in [web.md](web.md).
|
||||
@@ -0,0 +1,131 @@
|
||||
# Driving the native builds
|
||||
|
||||
Every native build has three entry points besides the editor window: the MCP
|
||||
server described in the [README](README.md) (`OpenCADStudio --mcp`), a headless
|
||||
JSON-line channel for scripts and CI (`OpenCADStudio --serve`), and a one-shot
|
||||
converter (`OpenCADStudio --export IN OUT`). The browser entry is described in
|
||||
[web.md](web.md).
|
||||
|
||||
## Choosing a channel
|
||||
|
||||
| | `--mcp` | `--serve` |
|
||||
|---|---|---|
|
||||
| Transport | MCP (JSON-RPC over stdio) with the four tools of the README | one JSON request per line on stdin, one reply per line on stdout; `--port N` listens on `127.0.0.1:N` instead |
|
||||
| Editor | attaches to a running desktop editor, or starts one when there is none (`ocs_sessions`, `launch_if_none`) | no window; a document session of its own that lives as long as the process |
|
||||
| Needs a display | yes (`xvfb-run` works on a headless machine; software rendering may raise the GPU warning dialog) | no |
|
||||
| Needs a writable `HOME` / `XDG_CONFIG_HOME` | yes — the session descriptors live in the user configuration directory; without one `ocs_sessions` answers `No user configuration directory` | no |
|
||||
| Startup dialogs | those of the editor (below) | none |
|
||||
| `capture` | `ocs_capture` | not available (`gui_required`): there is no window to capture |
|
||||
| Good for | an MCP client driving a real editor | batch jobs, CI, agents without a display |
|
||||
|
||||
## `--serve`
|
||||
|
||||
The first line written is a readiness banner, before any request is read:
|
||||
|
||||
```json
|
||||
{"ok":true,"ready":true,"version":"2026.38"}
|
||||
```
|
||||
|
||||
Over `--port N` the banner opens every connection; the listener serves one
|
||||
client at a time and the document session persists across reconnects.
|
||||
|
||||
Two request styles share the channel, told apart by the `protocol` field:
|
||||
|
||||
| Style | Shape | Operations |
|
||||
|---|---|---|
|
||||
| protocol 1 | `{"protocol":1,"request_id":"…","document_id":N,"op":"…"}` — the same requests and replies as `ocs_execute` / `ocs_read`, described in the README | `new`, `open`, `save`, `activate`, `select`, `run`, `start`, `input`, `cancel`, `stop`, `action`, `property`, `set_properties`, `embed_image`, `undo`, `redo`; reads `state`, `capabilities`, `query`, `entities`, `records`, `record_schema`, `layers`, `header`, `properties`, `measure`, `history`, `commands`, `events`, `operation` |
|
||||
| legacy | `{"op":"…"}` without `protocol` | `new`, `open`, `run`, `entities`, `query`, `records`, `record_schema`, `capabilities`, `layers`, `header`, `select`, `save`, `undo`, `redo` |
|
||||
|
||||
The two styles differ in how a waiting command is treated. A protocol-1 `run`
|
||||
or `start` while a command is still active is refused with `command_busy`:
|
||||
continue it with `input` or drop it with `cancel`. A legacy `run` line ends the
|
||||
waiting command and starts the new one.
|
||||
|
||||
### The legacy `run` line
|
||||
|
||||
`{"op":"run","cmd":"LINE 0,0 10,10"}` feeds the tokens after the command name
|
||||
to the prompts in order and then presses Enter. The reply is
|
||||
|
||||
```json
|
||||
{"ok":true,"cmd":"CIRCLE 0,0 5 9","status":"completed","blocked_by":null,"entities":1,"added":1,"unconsumed":["9"]}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `status` | `completed`, or `waiting_input` when the line left something open |
|
||||
| `blocked_by` | what *this line* left open: `"command"`, `"text_editor"`, `"mtext_editor"`, `"modal:<Kind>"`, or `null`. A surface that was already open when the line started is not attributed to it |
|
||||
| `entities`, `added` | entity count after the line, and the difference to before |
|
||||
| `unconsumed` | tokens no prompt asked for, in order (`["9"]` above: the radius step ended the command); `[]` when everything was consumed |
|
||||
|
||||
`waiting_input` means input is still required, not that nothing happened:
|
||||
`SPLINE` and `ARC` keep accepting input after the entity exists, and
|
||||
`zoom_extents` reports it after the view has already moved.
|
||||
|
||||
`TEXT 0,0 5 0 hello world` creates one `Text` entity holding `hello world`: the
|
||||
tail of the line is typed into the in-place editor, committed, and the command
|
||||
is closed so it does not stay armed for a second line. A `TEXT` line without
|
||||
content answers `waiting_input` with `blocked_by: "text_editor"`.
|
||||
|
||||
Known gap (#1390): `MTEXT`, `HATCH` and `POLYGON` answer `completed` with
|
||||
`added: 0` on this path — they need interactive surfaces (the rich-text editor,
|
||||
a pattern choice, a boundary pick) that a single line has no tokens to fill.
|
||||
|
||||
## `--mcp`
|
||||
|
||||
The MCP server is a client of the desktop editor: `ocs_sessions` lists the live
|
||||
editor sessions found in the configuration directory and starts one when none
|
||||
is running, so several MCP connections can share one editor and its state.
|
||||
|
||||
A fresh profile opens two dialogs before any drawing exists, and both block
|
||||
document creation:
|
||||
|
||||
| Step | `ocs_sessions` reports |
|
||||
|---|---|
|
||||
| right after launch | `modal: "AssocPrompt"` |
|
||||
| after `close_modal` | `modal: "DonationPrompt"` |
|
||||
| after a second `close_modal` | `modal: null` |
|
||||
|
||||
While a dialog is open, `{"op":"new"}` answers `ok` but no tab is created.
|
||||
Close the dialogs first:
|
||||
|
||||
```python
|
||||
while (info := sessions())["modal"]:
|
||||
execute({"op": "action", "name": "close_modal", "document_id": info["document_id"]})
|
||||
```
|
||||
|
||||
The donation prompt appears once per application version per profile, so an
|
||||
automated profile meets it once per version rather than on every run.
|
||||
|
||||
`new` makes the new drawing the active document. Requests carry `document_id`;
|
||||
take it from the reply's state and pass it on the following requests, as the
|
||||
README describes.
|
||||
|
||||
## `--export`
|
||||
|
||||
`OpenCADStudio --export IN OUT` loads `IN` and writes `OUT` without a window or
|
||||
a session. The output format follows the extension: `.dxf` writes DXF, any other
|
||||
extension writes DWG at the document's own version. The exit code is 0 on
|
||||
success and 1 when the input cannot be read or the output cannot be written.
|
||||
|
||||
## Interactive steps
|
||||
|
||||
For commands whose answers cannot be written on one line, use `start` and then
|
||||
`input`. `start` replies with `state.command`, whose `accepts`, `options` and
|
||||
`input_example` say what the current step wants:
|
||||
|
||||
| Step | Request | Note |
|
||||
|---|---|---|
|
||||
| a point | `{"op":"input","kind":"point","point":[x,y,0],"space":"wcs"}` | the point is an array; `space` is `wcs` (default), `ucs` or `relative` |
|
||||
| a typed value (height, rotation) | `{"op":"input","kind":"text","text":"5"}` | digits go through `text`, not `token`; `{"op":"input","kind":"enter"}` accepts the default |
|
||||
| a keyword (`J`, `ST`, `C`) | `{"op":"input","kind":"token","text":"C"}` | the payload field is named `text` |
|
||||
| an object | `{"op":"input","kind":"entity","handle":"HANDLE","point":[x,y,0]}` | `kind: "structure"` for a sub-entity pick, `kind: "selection"` to hand over the current selection |
|
||||
| the content of `TEXT` | `{"op":"action","name":"text_input","value":"hello"}` then `{"op":"action","name":"text_commit"}` | the command has already ended; the in-place editor owns the input |
|
||||
| the content of `MTEXT` | `{"op":"action","name":"mtext_insert","value":"hello"}` then `{"op":"action","name":"mtext_commit"}` | the rich-text editor has its own actions; the `TEXT` ones answer `editor_closed` |
|
||||
|
||||
## Native-only operations
|
||||
|
||||
| Operation | `--serve` (protocol 1) | `--mcp` |
|
||||
|---|---|---|
|
||||
| measure | `{"protocol":1,"op":"measure","handles":["2A"]}` — kernel-computed length, area, bounds and mass properties; no window needed | `ocs_read` with `op: "measure"` and `parameters.handles` |
|
||||
| capture | `{"protocol":1,"op":"capture","path":"out.png"}` — needs the editor window, so it answers `gui_required` here | `ocs_capture` with `scope` (`viewport` or `window`) and `max_dimension` |
|
||||
| `set_properties` | requires `collection`; colours are the serialised enum — `{"Index": 1}` or `"ByLayer"`; a colour name such as `"Red"` is rejected with `invalid_value` | same request through `ocs_execute` |
|
||||
@@ -0,0 +1,68 @@
|
||||
# Driving the web build
|
||||
|
||||
The web build exposes the same control core as `--serve` and `--mcp`, through two
|
||||
functions on the WebAssembly module. This page covers only what is specific to the
|
||||
browser; the operations, request envelope and replies are the ones in the
|
||||
[README](README.md).
|
||||
|
||||
## Reaching the control channel
|
||||
|
||||
The Trunk loader publishes the wasm-bindgen module as `window.wasmBindings`. The
|
||||
control channel is ready once `window.wasmBindings?.ocs_control_submit` is defined
|
||||
(the [save smoke test](web_save_smoke.cjs) waits for exactly that).
|
||||
|
||||
| Function | Behaviour |
|
||||
|---|---|
|
||||
| `ocs_control_submit(request: string) → string` | Queues one request (a JSON string) and returns a ticket immediately. |
|
||||
| `ocs_control_take(ticket: string) → string \| undefined` | Returns the reply (a JSON string) once it is ready, `undefined` before that. A reply is removed when taken. |
|
||||
|
||||
Poll `ocs_control_take` until it returns a reply, as the smoke test does:
|
||||
|
||||
```js
|
||||
async function control(request) {
|
||||
const api = window.wasmBindings;
|
||||
const ticket = api.ocs_control_submit(JSON.stringify(request));
|
||||
for (let i = 0; i < 600; i++) {
|
||||
const reply = api.ocs_control_take(ticket);
|
||||
if (reply) return JSON.parse(reply);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
throw new Error('control timed out');
|
||||
}
|
||||
```
|
||||
|
||||
Limits: at most 64 requests wait in the queue (a further `submit` is answered with the
|
||||
`busy` error), and only the newest 64 replies are kept, so take a reply soon after it is
|
||||
ready. A request that is not valid JSON is answered with `op: "invalid"` and a
|
||||
`parse_error`.
|
||||
|
||||
## Opening a drawing from bytes
|
||||
|
||||
The web build has no file system, so `open` takes the file's bytes instead of a path:
|
||||
|
||||
```json
|
||||
{"op":"open","name":"plan.dwg","data_base64":"…"}
|
||||
```
|
||||
|
||||
`name` is reduced to its file name and labels the tab and the recent-files entry;
|
||||
`data_base64` must be valid base64 and non-empty (`invalid_request` otherwise). While an
|
||||
earlier drawing is still opening the request is answered with `busy`; retry it once
|
||||
`state` shows the tab. A `path` is resolved against the browser's "recent files" store
|
||||
rather than a disk, so use `data_base64` for a file the page has not seen.
|
||||
`data_base64` is rejected on native builds; use `path` there.
|
||||
|
||||
## Startup dialogs
|
||||
|
||||
A dialog can be open when the page is ready. The donation prompt appears once per
|
||||
application version in each browser profile, so a fresh or disposable profile always
|
||||
sees it. While a modal is open `state.modal` is set, and `{"op":"new"}` returns `ok`
|
||||
without creating a tab until the modal is closed. Close it first:
|
||||
|
||||
```js
|
||||
const state = await control({ op: 'state' });
|
||||
if (state.modal) await control({ op: 'action', name: 'close_modal' });
|
||||
await control({ op: 'new' });
|
||||
```
|
||||
|
||||
Native builds can show a second dialog at launch (the file-association prompt), so check
|
||||
`state.modal` again after closing one.
|
||||
@@ -109,11 +109,13 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `STRETCH` | | current selection or prompt |
|
||||
| `TRIM` | | command-driven |
|
||||
|
||||
## Other (514)
|
||||
## Other (529)
|
||||
|
||||
| Command | Default shortcut | Selection |
|
||||
|---|---|---|
|
||||
| `-PSETUPIN` | | command-driven |
|
||||
| `-SETBYLAYER` | | command-driven |
|
||||
| `-XREF` | | command-driven |
|
||||
| `3DALIGN` | | command-driven |
|
||||
| `3DARRAY` | | command-driven |
|
||||
| `3DFACE` | | command-driven |
|
||||
@@ -174,6 +176,7 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `AUPREC` | | command-driven |
|
||||
| `AUTOCONSTRAIN` | | command-driven |
|
||||
| `BACKGROUND` | | command-driven |
|
||||
| `BACKGROUNDPLOT` | | command-driven |
|
||||
| `BASE` | | command-driven |
|
||||
| `BATTMAN` | | command-driven |
|
||||
| `BEDIT` | | command-driven |
|
||||
@@ -188,6 +191,7 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `BMAKE` | | command-driven |
|
||||
| `BOUNDARY` | | command-driven |
|
||||
| `BREAKATPOINT` | | command-driven |
|
||||
| `BROWSER` | | command-driven |
|
||||
| `BYLAYER` | | command-driven |
|
||||
| `CAL` | | command-driven |
|
||||
| `CANNOSCALE` | | command-driven |
|
||||
@@ -224,6 +228,7 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `COLORTHEME` | | command-driven |
|
||||
| `COLOUR` | | command-driven |
|
||||
| `COMMANDLINEFADETIME` | | command-driven |
|
||||
| `CONSTRAINTBAR` | | command-driven |
|
||||
| `CONSTRAINTSETTINGS` | | command-driven |
|
||||
| `CONSTRUCTIONLINE` | | command-driven |
|
||||
| `CONTENTBROWSER` | | command-driven |
|
||||
@@ -233,7 +238,9 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `COPYCLIP` | Ctrl+C | current selection or prompt |
|
||||
| `COUNT` | | command-driven |
|
||||
| `CPCONSTRAINT` | | command-driven |
|
||||
| `CREATESKETCH` | | command-driven |
|
||||
| `CROSSINGAREACOLOR` | | command-driven |
|
||||
| `CTAB` | | command-driven |
|
||||
| `CUI` | | command-driven |
|
||||
| `CUIEXPORT` | | command-driven |
|
||||
| `CUIIMPORT` | | command-driven |
|
||||
@@ -265,7 +272,6 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `DDPTYPE` | | command-driven |
|
||||
| `DDUNITS` | | command-driven |
|
||||
| `DE` | | command-driven |
|
||||
| `DELCONSTRAINT` | | command-driven |
|
||||
| `DELOBJ` | | command-driven |
|
||||
| `DESELALL` | | command-driven |
|
||||
| `DESELECT` | | command-driven |
|
||||
@@ -286,7 +292,6 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `DWGPROPS` | | command-driven |
|
||||
| `DWGUNITS` | | command-driven |
|
||||
| `EATTEXT` | | command-driven |
|
||||
| `ECONSTRAINT` | | command-driven |
|
||||
| `EDCONSTRAINT` | | command-driven |
|
||||
| `EDGE` | | command-driven |
|
||||
| `ELEVATION` | | command-driven |
|
||||
@@ -298,6 +303,7 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `EXPORTPDF` | | command-driven |
|
||||
| `EXPORTSTEP` | | command-driven |
|
||||
| `EXPORTSTL` | | command-driven |
|
||||
| `EXTERNALREFERENCES` | | command-driven |
|
||||
| `EXTRIM` | | command-driven |
|
||||
| `FI` | | command-driven |
|
||||
| `FILEASSOC` | | command-driven |
|
||||
@@ -306,6 +312,7 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `FILTER` | | command-driven |
|
||||
| `FIND` | Ctrl+F | command-driven |
|
||||
| `FINDNONPURGEABLE` | | command-driven |
|
||||
| `FINISHSKETCH` | | command-driven |
|
||||
| `FITSPLINE` | | command-driven |
|
||||
| `FLATSHOT` | | command-driven |
|
||||
| `FLATTEN` | | command-driven |
|
||||
@@ -314,13 +321,14 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `FRAMES1` | | command-driven |
|
||||
| `FRAMES2` | | command-driven |
|
||||
| `FXCONSTRAINT` | | command-driven |
|
||||
| `GCCOINCIDENT` | | command-driven |
|
||||
| `GCFIX` | | command-driven |
|
||||
| `GCHIDE` | | command-driven |
|
||||
| `GCHIDEALL` | | command-driven |
|
||||
| `GCRESET` | | command-driven |
|
||||
| `GCSHOW` | | command-driven |
|
||||
| `GCSHOWALL` | | command-driven |
|
||||
| `GEOMCONSTRAINT` | | command-driven |
|
||||
| `GCSMOOTH` | | command-driven |
|
||||
| `GCVERTICAL` | | command-driven |
|
||||
| `GRADIENT` | | command-driven |
|
||||
| `GRID` | F7 | command-driven |
|
||||
| `GRIPCOLOR` | | command-driven |
|
||||
@@ -333,7 +341,6 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `HATCHEDIT` | | command-driven |
|
||||
| `HATCHTOBACK` | | command-driven |
|
||||
| `HB` | | command-driven |
|
||||
| `HCONSTRAINT` | | command-driven |
|
||||
| `HELIX` | | command-driven |
|
||||
| `HELP` | F1 | command-driven |
|
||||
| `HI` | | command-driven |
|
||||
@@ -344,6 +351,7 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `IM` | | command-driven |
|
||||
| `IMAGE` | | command-driven |
|
||||
| `IMAGEATTACH` | | command-driven |
|
||||
| `IMAGEEMBED` | | command-driven |
|
||||
| `IMAGEFRAME` | | command-driven |
|
||||
| `IMPORTOBJ` | | command-driven |
|
||||
| `INF` | | command-driven |
|
||||
@@ -379,7 +387,6 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `LAYTRANS` | | command-driven |
|
||||
| `LAYULK` | | current selection or prompt |
|
||||
| `LAYUNISO` | | command-driven |
|
||||
| `LCONSTRAINT` | | command-driven |
|
||||
| `LENGTHEN` | | command-driven |
|
||||
| `LI` | | command-driven |
|
||||
| `LIMCHECK` | | command-driven |
|
||||
@@ -413,11 +420,9 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `MULTIPOINT` | | command-driven |
|
||||
| `MVIEW` | | command-driven |
|
||||
| `NAVVCUBE` | | command-driven |
|
||||
| `NCONSTRAINT` | | command-driven |
|
||||
| `NCOPY` | | command-driven |
|
||||
| `NCOPYALL` | | command-driven |
|
||||
| `NEW` | Ctrl+N | command-driven |
|
||||
| `NRCONSTRAINT` | | command-driven |
|
||||
| `OBJECTSCALE` | | command-driven |
|
||||
| `OBJIMPORT` | | command-driven |
|
||||
| `OCCONSTRAINT` | | command-driven |
|
||||
@@ -431,12 +436,12 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `OVERKILL` | | command-driven |
|
||||
| `P` | | command-driven |
|
||||
| `PAGESETUP` | | command-driven |
|
||||
| `PAPERUPDATE` | | command-driven |
|
||||
| `PARAMETERS` | | command-driven |
|
||||
| `PASTE` | | command-driven |
|
||||
| `PASTEBLOCK` | Ctrl+SHIFT+V | command-driven |
|
||||
| `PASTECLIP` | Ctrl+V | command-driven |
|
||||
| `PASTEORIG` | | command-driven |
|
||||
| `PCONSTRAINT` | | command-driven |
|
||||
| `PDFATTACH` | | command-driven |
|
||||
| `PDFFRAME` | | command-driven |
|
||||
| `PDMODE` | | command-driven |
|
||||
@@ -450,9 +455,12 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `PLIMCHECK` | | command-driven |
|
||||
| `PLINEGEN` | | command-driven |
|
||||
| `PLOT` | Ctrl+P | command-driven |
|
||||
| `PLOTOFFSET` | | command-driven |
|
||||
| `PLOTROTMODE` | | command-driven |
|
||||
| `PLOTSTYLE` | | command-driven |
|
||||
| `PLOTSTYLEEDITOR` | | command-driven |
|
||||
| `PLOTSTYLEPANEL` | | command-driven |
|
||||
| `PLOTTRANSPARENCYOVERRIDE` | | command-driven |
|
||||
| `PLOTWINDOW` | | command-driven |
|
||||
| `PLUGINMANAGER` | | command-driven |
|
||||
| `PLUGINS` | | command-driven |
|
||||
@@ -465,15 +473,16 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `PR` | | command-driven |
|
||||
| `PRINT` | | command-driven |
|
||||
| `PRINTALL` | | command-driven |
|
||||
| `PRINTERS` | | command-driven |
|
||||
| `PROPERTIES` | Ctrl+1 | command-driven |
|
||||
| `PROPS` | | command-driven |
|
||||
| `PSETUPIN` | | command-driven |
|
||||
| `PSLTSCALE` | | command-driven |
|
||||
| `PSPACE` | | command-driven |
|
||||
| `PURGE` | | command-driven |
|
||||
| `PYR` | | command-driven |
|
||||
| `PYRAMID` | | command-driven |
|
||||
| `QC` | | command-driven |
|
||||
| `QCONSTRAINT` | | command-driven |
|
||||
| `QDIM` | | command-driven |
|
||||
| `QL` | | command-driven |
|
||||
| `QLEADER` | | command-driven |
|
||||
@@ -531,6 +540,8 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `SHADEMODE` | | command-driven |
|
||||
| `SHEETSET` | | command-driven |
|
||||
| `SHELL` | | command-driven |
|
||||
| `SHORTCUTMENU` | | command-driven |
|
||||
| `SHORTCUTMENUDURATION` | | command-driven |
|
||||
| `SHORTCUTS` | | command-driven |
|
||||
| `SKETCH` | | command-driven |
|
||||
| `SKETCHINC` | | command-driven |
|
||||
@@ -538,7 +549,6 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `SKTOLERANCE` | | command-driven |
|
||||
| `SL` | | command-driven |
|
||||
| `SLICE` | | command-driven |
|
||||
| `SMOOTHCONSTRAINT` | | command-driven |
|
||||
| `SNAP` | F9 | command-driven |
|
||||
| `SNAPANG` | | command-driven |
|
||||
| `SO` | | command-driven |
|
||||
@@ -548,6 +558,12 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `SOLIDEDIT` | | command-driven |
|
||||
| `SOLIDFILLET` | | command-driven |
|
||||
| `SORTENTS` | | command-driven |
|
||||
| `SPACEMOUSE` | | command-driven |
|
||||
| `SPACEMOUSE3D` | | command-driven |
|
||||
| `SPACEMOUSEAUTO` | | command-driven |
|
||||
| `SPACEMOUSEPAN` | | command-driven |
|
||||
| `SPACEMOUSEPANZOOM` | | command-driven |
|
||||
| `SPACEMOUSEPAUSE` | | command-driven |
|
||||
| `SPLFRAME` | | command-driven |
|
||||
| `SPLINECV` | | command-driven |
|
||||
| `SPLINEFIT` | | command-driven |
|
||||
@@ -567,7 +583,6 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `TABLEDIT` | | command-driven |
|
||||
| `TABLESTYLE` | | command-driven |
|
||||
| `TCASE` | | command-driven |
|
||||
| `TCONSTRAINT` | | command-driven |
|
||||
| `TCOUNT` | | command-driven |
|
||||
| `TEDIT` | | command-driven |
|
||||
| `TEXTEDIT` | | command-driven |
|
||||
@@ -580,6 +595,7 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `TEXTTOFRONT` | | command-driven |
|
||||
| `THICKEN` | | command-driven |
|
||||
| `THICKNESS` | | command-driven |
|
||||
| `TILEMODE` | | command-driven |
|
||||
| `TOOLPALETTES` | | command-driven |
|
||||
| `TORIENT` | | command-driven |
|
||||
| `TP` | | command-driven |
|
||||
@@ -597,7 +613,6 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `USERI` | | command-driven |
|
||||
| `USERR` | | command-driven |
|
||||
| `USRTIMER` | | command-driven |
|
||||
| `VCONSTRAINT` | | command-driven |
|
||||
| `VERTICAL` | | command-driven |
|
||||
| `VIEW` | | command-driven |
|
||||
| `VISRETAIN` | | command-driven |
|
||||
@@ -628,6 +643,25 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `ZOOMWHEEL` | | command-driven |
|
||||
| `ZS` | | command-driven |
|
||||
|
||||
## Parametric (14)
|
||||
|
||||
| Command | Default shortcut | Selection |
|
||||
|---|---|---|
|
||||
| `DELCONSTRAINT` | | command-driven |
|
||||
| `ECONSTRAINT` | | command-driven |
|
||||
| `GCCOINCIDENT` | | command-driven |
|
||||
| `GCCONCENTRIC` | | command-driven |
|
||||
| `GCEQUAL` | | command-driven |
|
||||
| `GCHORIZONTAL` | | command-driven |
|
||||
| `GCPERPENDICULAR` | | command-driven |
|
||||
| `GEOMCONSTRAINT` | | command-driven |
|
||||
| `LCONSTRAINT` | | command-driven |
|
||||
| `NRCONSTRAINT` | | command-driven |
|
||||
| `PCONSTRAINT` | | command-driven |
|
||||
| `QCONSTRAINT` | | command-driven |
|
||||
| `TCONSTRAINT` | | command-driven |
|
||||
| `VCONSTRAINT` | | command-driven |
|
||||
|
||||
## View (6)
|
||||
|
||||
| Command | Default shortcut | Selection |
|
||||
@@ -639,4 +673,4 @@ Generated from the in-repo command registry; manual edits are overwritten. Regen
|
||||
| `VPORTS` | | command-driven |
|
||||
| `ZOOM` | | command-driven |
|
||||
|
||||
_602 commands across 7 categories._
|
||||
_631 commands across 8 categories._
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 166 KiB |
@@ -0,0 +1,38 @@
|
||||
# Perspective draw-order clipping regression
|
||||
|
||||
Switching a flat model view from orthographic to perspective could hide otherwise
|
||||
visible lines. The camera applied an OpenGL-to-WebGPU depth conversion to a
|
||||
projection that already used WebGPU's depth range. The shaders then applied a
|
||||
fixed draw-order offset that could push geometry past the far clipping boundary.
|
||||
|
||||
The camera now uses the DirectX/WebGPU projection directly. The shared
|
||||
`draw_order.wgsl` function keeps the existing offset away from clipping planes
|
||||
and reduces it near either plane. Its adjustment is monotonic in draw order,
|
||||
leaves neutral geometric depth unchanged, and does not move clipped vertices
|
||||
back into range. It is shared by the native and compatibility shader paths.
|
||||
|
||||
## GPU reproduction
|
||||
|
||||
These are offscreen renders from the application's wire pipelines. All 17 lines
|
||||
are on the same plane; only their draw-order values differ. The first two lines
|
||||
(red when visible) were clipped in perspective. Both projections now show all
|
||||
17 lines. This synthetic drawing contains no user CAD geometry.
|
||||
|
||||
| Before | After |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
cargo test --locked --lib
|
||||
cargo test --locked --lib scene::pipeline::depth_tests -- --ignored --nocapture
|
||||
```
|
||||
|
||||
The GPU tests exercise storage and packed wire pipelines, near/far clipping,
|
||||
coplanar draw order in both submission orders, and wire occlusion by a triangle
|
||||
mesh. Normal tests check camera depth endpoints, picking, and validation of all
|
||||
14 composed draw-order shaders.
|
||||
|
||||
Set `OCS_DEPTH_TEST_IMAGES` to an output directory to save the orthographic and
|
||||
perspective reproduction images while running the GPU tests.
|
||||
@@ -101,13 +101,16 @@ affect whether a prebuilt plugin can safely be loaded:
|
||||
|
||||
A plugin's `plugin.toml` declares the same values under `[opencad]`. For API v4
|
||||
and later, `rustc_version` is required and the host refuses to load the plugin
|
||||
unless it matches exactly. This turns the current silent runner crash into a
|
||||
one-line diagnosis such as:
|
||||
unless it matches exactly — omitting it is treated as a mismatch, not skipped.
|
||||
This turns the current silent runner crash into a one-line diagnosis such as:
|
||||
|
||||
```
|
||||
Plugin built with rustc 1.96.0, host requires 1.98.0 - rebuild required
|
||||
```
|
||||
|
||||
`acadrust_source` is also required for API v4 and later. Missing, empty, or
|
||||
mismatched metadata makes the package incompatible.
|
||||
|
||||
### `PluginManifest`
|
||||
|
||||
```rust
|
||||
@@ -202,11 +205,14 @@ struct ExampleModule;
|
||||
impl CadModule for ExampleModule {
|
||||
fn id(&self) -> &'static str { "example" }
|
||||
fn title(&self) -> &'static str { "Example" }
|
||||
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||
vec![RibbonGroup { title: "Demo", tools: vec![RibbonItem::LargeTool(ToolDef {
|
||||
// Returns a slice, not an owned Vec — build once into a static and hand
|
||||
// back a reference, rather than rebuilding (and dropping) it every call.
|
||||
fn ribbon_groups(&self) -> &[RibbonGroup] {
|
||||
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
|
||||
GROUPS.get_or_init(|| vec![RibbonGroup { title: "Demo", tools: vec![RibbonItem::LargeTool(ToolDef {
|
||||
id: "EX_HELLO", label: "Hello", icon: IconKind::Glyph("◆"),
|
||||
event: ModuleEvent::Command("EX_HELLO".to_string()),
|
||||
})]}]
|
||||
})]}]})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,8 +241,9 @@ api_version = 5
|
||||
ribbon_order = 50
|
||||
command_prefixes = ["EX_"]
|
||||
xdata_apps = []
|
||||
# Filled with the exact compiler output while staging the release asset.
|
||||
# Both are filled at release-build time; see the CI step below.
|
||||
rustc_version = "rustc 1.98.0 (hash date)"
|
||||
acadrust_source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=<short>#<full-40-char-commit>"
|
||||
```
|
||||
|
||||
The full, buildable scaffold is in [`docs/plugin-template/`](plugin-template);
|
||||
@@ -255,6 +262,10 @@ tool fires `ModuleEvent::Command("EX_FOO")`, which round-trips to
|
||||
tool request a native file picker; on selection the host dispatches
|
||||
`"<command> <path>"` back with the path's original case preserved.
|
||||
|
||||
A leading `>` or the persistent literal-input toggle preserves both spaces
|
||||
and case. Automation and `ModuleEvent::PluginFileDialog` already preserve
|
||||
case without this prefix.
|
||||
|
||||
### Interactive (click-to-place) commands
|
||||
|
||||
For tools that collect points — placing a structure, drawing a pipe — call
|
||||
@@ -306,7 +317,18 @@ plugin.toml
|
||||
```
|
||||
|
||||
A GitHub Actions matrix workflow (see the example repo / template) cross-builds
|
||||
and uploads these on a `v*` tag.
|
||||
and uploads these on a `v*` tag. It also fills `plugin.toml`'s `rustc_version`
|
||||
and `acadrust_source` placeholders from the CI run's actual toolchain and
|
||||
resolved dependency, rather than hand-editing them per release — see
|
||||
`docs/plugin-template/.github/workflows/release.yml` for the exact `sed`
|
||||
substitutions, including reading `acadrust_source` out of `Cargo.lock`'s
|
||||
resolved `acadrust` package entry (`source = "git+...#<hash>"`) since that's
|
||||
the same value the host embeds at its own build time. The template's CI pins
|
||||
`rust-toolchain@stable`, which is a floating target — a real release pipeline
|
||||
should pin the exact toolchain version the host was built with (check
|
||||
`crates/ocs_plugin_api/src/version_info.rs`'s embedded value, or the host's
|
||||
own CI config) rather than trusting "whatever's current stable today" to
|
||||
keep matching it.
|
||||
|
||||
---
|
||||
|
||||
@@ -383,6 +405,9 @@ dynamic libraries:
|
||||
- **Enable/disable** — toggles a loaded plugin's ribbon tab + dispatch without
|
||||
uninstalling.
|
||||
|
||||
One linked repository is one marketplace entry. Publish separate plugins from
|
||||
separate repositories so release and update histories stay unambiguous.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility & ABI
|
||||
@@ -391,9 +416,9 @@ Plugins are loaded as `cdylib`s by a plugin-runner child process. The host spawn
|
||||
this child from its own executable (`--ocs-plugin-runner` mode), so the runner
|
||||
and host always share the same `ocs_plugin_api` build. The runner checks
|
||||
`ocs_plugin_api_version` before any plugin code runs. Each plugin runs in its
|
||||
own OS process, so the host is protected from plugin crashes and memory
|
||||
corruption. Process isolation removes the need for the host and plugin to share
|
||||
a Rust toolchain ABI beyond the stable `ocs_plugin_api` contract.
|
||||
own OS process, so a plugin crash or memory-corruption bug cannot take down the
|
||||
host process itself. The runner loads the plugin library in-process, however,
|
||||
so API v4+ requires matching `rustc_version` and `acadrust_source` metadata.
|
||||
|
||||
A future hardening step is a `#[repr(C)]` vtable (a true C ABI) so binaries built
|
||||
by any toolchain interoperate — required before trusting prebuilt binaries from
|
||||
@@ -414,6 +439,12 @@ Done:
|
||||
reinstall / uninstall, enable/disable.
|
||||
- [x] Interactive command round-trip over IPC (prompt, point/enter/object-pick).
|
||||
- [x] Spawn and per-call IPC timeouts so a stuck runner cannot freeze the host.
|
||||
- [x] External automation (`--mcp` — local stdio MCP/JSON-RPC server for AI
|
||||
clients; `--serve` — headless JSON automation server; both drive OCS
|
||||
from a separate process without the GUI, per `src/cli.rs`/`src/mcp.rs`/
|
||||
`app::serve`). This closes the "drive OCS headless from a process" goal
|
||||
of issue #29; this line previously and incorrectly listed it as not yet
|
||||
started.
|
||||
|
||||
Next:
|
||||
|
||||
@@ -421,7 +452,6 @@ Next:
|
||||
- [ ] `#[repr(C)]` vtable / strict handshake for cross-toolchain binaries.
|
||||
- [ ] Trust: checksums / signatures before spawning plugin processes.
|
||||
- [ ] Interchange (LandXML / SWMM) and live `on_entity_committed` hooks.
|
||||
- [ ] External automation API (drive OCS headless from a process) — issue #29.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -12,4 +12,4 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
ocs_plugin_api = { path = "../../crates/ocs_plugin_api", features = ["host"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "568a12c" }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a28c21" }
|
||||
@@ -64,8 +64,10 @@ From the workspace root:
|
||||
cargo build -p plugin-template-v2
|
||||
```
|
||||
|
||||
The resulting cdylib plus `plugin.toml` can be installed into the host's
|
||||
plugins folder (see `docs/plugin-architecture.md`).
|
||||
The resulting cdylib plus `plugin.toml` (checked in alongside this file) can
|
||||
be installed into the host's plugins folder (see `docs/plugin-architecture.md`).
|
||||
Replace `plugin.toml`'s `rustc_version` and `acadrust_source` placeholders in
|
||||
your packaging step. The release workflow in `docs/plugin-template` shows how.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[plugin]
|
||||
id = "com.example.plugin-template-v2"
|
||||
name = "Plugin Template v2"
|
||||
version = "0.1.0"
|
||||
description = "Demonstrates read/write object round-trips with the host."
|
||||
author = "Your Name"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "your-account/your-plugin-repo"
|
||||
|
||||
[opencad]
|
||||
api_version = 5
|
||||
# Replace both placeholders in the packaging step. The v1 template's
|
||||
# release workflow contains a working example.
|
||||
rustc_version = "__RUSTC_VERSION__"
|
||||
acadrust_source = "__ACADRUST_SOURCE__"
|
||||
ribbon_order = 100
|
||||
command_prefixes = [
|
||||
"COUNT_SURVEY_POINTS",
|
||||
"ADD_SURVEY_POINT",
|
||||
"MARK_FIRST_SURVEY_POINT",
|
||||
"COUNT_MARKED_SURVEY_POINTS",
|
||||
]
|
||||
xdata_apps = ["SURVEYMARK"]
|
||||
@@ -29,6 +29,14 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# NOTE: @stable floats to whatever's current when this job runs, which
|
||||
# is NOT the same thing as "matches the host's rustc exactly" — the API
|
||||
# v4+ load check compares plugin.toml's rustc_version to the host's
|
||||
# build-time compiler byte-for-byte (see plugin-architecture.md's
|
||||
# "Build-time ABI metadata"). Pin this to the host's actual toolchain
|
||||
# version (check crates/ocs_plugin_api/src/version_info.rs's embedded
|
||||
# value, or the host's own CI config) before a real release, or your
|
||||
# binary may be refused at load with a version-mismatch error.
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo build --release
|
||||
- name: Stage release asset
|
||||
@@ -36,7 +44,11 @@ jobs:
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp target/release/*.${{ matrix.ext }} "dist/${{ matrix.asset }}"
|
||||
sed "s|__RUSTC_VERSION__|$(rustc --version)|" plugin.toml > dist/plugin.toml
|
||||
acadrust_source=$(grep -A3 '^name = "acadrust"' Cargo.lock | grep '^source' | head -1 | sed 's/^source = "//;s/"$//')
|
||||
sed \
|
||||
-e "s|__RUSTC_VERSION__|$(rustc --version)|" \
|
||||
-e "s|__ACADRUST_SOURCE__|$acadrust_source|" \
|
||||
plugin.toml > dist/plugin.toml
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -10,6 +10,8 @@ repository = "your-account/your-plugin-repo"
|
||||
[opencad]
|
||||
api_version = 5
|
||||
rustc_version = "__RUSTC_VERSION__"
|
||||
# release.yml fills both placeholders from the compiler and Cargo.lock.
|
||||
acadrust_source = "__ACADRUST_SOURCE__"
|
||||
ribbon_order = 60
|
||||
command_prefixes = ["MP_"]
|
||||
xdata_apps = ["MYPLUGIN_RECORD"]
|
||||
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 133 KiB |
@@ -0,0 +1,145 @@
|
||||
# SpaceMouse
|
||||
|
||||
The Windows desktop app supports 3Dconnexion SpaceMouse devices through the
|
||||
installed [3DxWare driver](https://3dconnexion.com/drivers/). Open CAD Studio
|
||||
loads NavLib from the Windows system directory at runtime. An SDK installation
|
||||
is not needed to build or run the app. macOS, Linux, and the web build currently
|
||||
show an unavailable state; their device adapters are not implemented.
|
||||
|
||||
## Navigation
|
||||
|
||||
Open **Options → User Preferences → SpaceMouse**, or enter `SPACEMOUSE`.
|
||||
|
||||
| Mode | Behavior |
|
||||
| --- | --- |
|
||||
| Follow context | Pan and zoom on a paper sheet; pan, zoom, and orbit in model views. |
|
||||
| Pan only | Push or tilt the puck to pan in the current screen plane. Twist and vertical pressure are ignored. Scale, depth, and orientation stay fixed, including in perspective. |
|
||||
| Pan and zoom | Uses the same push/tilt panning as Pan only. Lift or press the puck to zoom about the view center. Twist is ignored and orientation stays fixed. |
|
||||
| 3D navigation | Pan, zoom, and orbit in model views. Paper sheets keep orientation fixed. |
|
||||
|
||||
For a flat drawing, choose **Pan only** to use the puck as a panning joystick.
|
||||
Push or tilt right/left to move the drawing right/left; push or tilt away/toward
|
||||
you to move it up/down. Pushing and tilting together does not double the speed.
|
||||
Enable **Reverse pan direction** if you prefer moving the viewpoint in the same
|
||||
direction as the puck. **Reverse pan direction** and **Pan speed** apply to both
|
||||
flat-navigation modes, including paper sheets in Follow context. A small dead zone
|
||||
filters incidental forces, and releasing the puck stops movement.
|
||||
The app does not infer that a model-space file is 2D from its extension or from
|
||||
the current camera angle. These preferences belong to the user and persist
|
||||
across drawings and sessions.
|
||||
|
||||
The status bar shows the effective mode after a device connects, and keeps the
|
||||
control available after disconnection. An explicitly selected navigation mode
|
||||
also makes it visible. Its menu provides mode selection, Pause/Resume, preferences,
|
||||
and a shortcut to 3Dconnexion settings. Status-bar customization can hide it.
|
||||
|
||||
Only the focused drawing receives input. Dialogs suspend navigation. The
|
||||
view metadata remains available to the driver while its settings UI has focus.
|
||||
Native 3DxWare chooses the foreground application; the adapter marks its view
|
||||
active without forcing the NLServer-specific keyboard-focus property. Entered
|
||||
paper-space viewports use their own cameras; their display locks are respected.
|
||||
The active model tile receives navigation in a split view. Moving the puck
|
||||
preserves an unfinished drawing command, refreshes its preview at a stationary
|
||||
pointer, and keeps an active selection-box anchor attached to the drawing.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Buttons and radial menus
|
||||
|
||||
Use **Open 3Dconnexion settings…** to assign buttons and radial-menu entries.
|
||||
The app exports registered CAD command IDs and reuses ribbon labels and icons
|
||||
where available. Speed, axis inversion, dominant-axis filtering, and radial-menu
|
||||
layout for SDK navigation remain in the driver settings. Pan only and Pan and zoom
|
||||
share the app's push/tilt mapping and Pan speed control; driver axis remappings do
|
||||
not apply to these flat-navigation modes. Button assignments continue to work in
|
||||
all modes.
|
||||
|
||||
For orbiting with a fixed up direction, enable **Lock Horizon** in the driver's
|
||||
Advanced Settings for Open CAD Studio. The adapter reads `settings.LockHorizon`
|
||||
through NavLib at connection/focus and after `settings.changed` notifications.
|
||||
When enabled, accepted orbit matrices remove roll relative to world Z. Pitch
|
||||
stops just short of the top/bottom pole, retaining heading instead of flipping
|
||||
the view. In the driver's Object mode the eye receives the same pitch correction
|
||||
around the SDK's orbit pivot, so the object stays in place; Camera/Helicopter
|
||||
modes retain their proposed eye position. Disabling Lock Horizon restores
|
||||
unrestricted driver rotation.
|
||||
The app does not duplicate this control, the driver's rotation lock, or
|
||||
its object/camera navigation modes.
|
||||
|
||||
Useful actions include Undo, Redo, Cancel, Fit view, Top view, and these commands:
|
||||
|
||||
| Command | Action |
|
||||
| --- | --- |
|
||||
| `SPACEMOUSE` | Open SpaceMouse preferences |
|
||||
| `SPACEMOUSEPAUSE` | Pause/resume puck navigation for this session |
|
||||
| `SPACEMOUSEAUTO` | Follow context |
|
||||
| `SPACEMOUSEPAN` | Pan only |
|
||||
| `SPACEMOUSEPANZOOM` | Pan and zoom |
|
||||
| `SPACEMOUSE3D` | 3D navigation |
|
||||
|
||||
Fit view (`SPACEMOUSEFIT`) and Top view (`SPACEMOUSETOP`) are also available as
|
||||
button/keyboard actions. Button actions share the keyboard dispatcher: for
|
||||
example, Undo removes the last entered point of an unfinished polyline. A
|
||||
Pause/Resume button remains usable while puck navigation is paused.
|
||||
|
||||
## Implementation
|
||||
|
||||
- `src/input/spacemouse/`: portable preferences, camera snapshots, and a bounded
|
||||
thread-safe bridge. `navlib.rs` contains the Windows ABI and runtime loading.
|
||||
`raw_input.rs` receives SpaceMouse multi-axis HID reports for planar navigation;
|
||||
`pan.rs` maps pushes and tilts to screen velocity and vertical pressure to zoom.
|
||||
The planar path suppresses SDK camera updates so one gesture cannot be applied
|
||||
twice. Only 3Dconnexion/legacy Logitech multi-axis
|
||||
controllers are accepted. Other HID devices and report types are ignored.
|
||||
`worker.rs` waits for app work and dispatches Windows messages on the SDK's
|
||||
owning thread, including settings changes while the drawing is unfocused.
|
||||
- `src/app/navigation.rs`: focus/context policy, cached selection bounds, camera
|
||||
application, pivot feedback, and the action catalog.
|
||||
- `src/scene/mspace.rs`: routing to the active model or floating-viewport camera.
|
||||
- `src/ui/window/options/spacemouse.rs` and `src/ui/statusbar/spacemouse.rs`:
|
||||
controls within the existing Options and status-menu systems.
|
||||
- `src/app/settings.rs`: preferences; `src/app/shortcuts.rs`: shared actions.
|
||||
|
||||
Driver callbacks operate on snapshots and never access the scene or GUI. A
|
||||
navigation transaction is coalesced into one pending camera update. Target
|
||||
stamps discard late motion after a tab, layout, viewport, tile, or camera change.
|
||||
Acknowledging an applied frame retains newer callbacks from the same gesture;
|
||||
an external camera change resets the SDK navigation model. Raw pan state is
|
||||
cleared on context/focus changes, pause, disconnection, and stale reports.
|
||||
Button requests retain their context across camera revisions. Losing focus or
|
||||
changing drawing context discards queued input. Connection shutdown closes
|
||||
NavLib before releasing callback, command, image, or library storage.
|
||||
The subscription joins its worker at shutdown so NavLib can finish persisting
|
||||
the driver's per-application preferences before the process exits.
|
||||
|
||||
For driver-setting diagnostics, set
|
||||
`RUST_LOG=OpenCADStudio::input::spacemouse=debug` before launching. The trace
|
||||
records settings-change notifications and the Lock Horizon value read from
|
||||
NavLib; it does not change the user's driver settings.
|
||||
|
||||
Iced supplies monotonic frame timestamps while navigation is active. Idle
|
||||
drawings do not request continuous rendering. Driver-originated camera updates
|
||||
are not echoed back as external-camera notifications, which would reset the
|
||||
driver's navigation model during a held input.
|
||||
The optional SDK `view.target` property returns no-data: the app's camera center
|
||||
does not represent the SDK's separate target-camera navigation model.
|
||||
|
||||
The adapter uses the NavLib C ABI described in the 3DxWare SDK headers
|
||||
(`navlib.h`, `navlib_types.h`, and `siappcmd_types.h`). No vendor headers or
|
||||
runtime binaries are included. SpaceMouse and 3Dconnexion are trademarks of
|
||||
3Dconnexion.
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
cargo test --locked --lib spacemouse
|
||||
```
|
||||
|
||||
An opt-in smoke test checks the installed driver, command/icon export, device
|
||||
presence, and reading its Lock Horizon setting:
|
||||
|
||||
```sh
|
||||
cargo test --locked --lib installed_driver_connects_and_reports_device_presence -- --ignored --nocapture
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="350" viewBox="0 0 900 350">
|
||||
<style>text{font-family:Arial,sans-serif;fill:#263238} .title{font-size:22px;font-weight:600} .label{font-size:16px} .small{font-size:13px;fill:#54616a} .model{stroke:#1479b8;stroke-width:4;fill:none} .paper{stroke:#a94610;stroke-width:2;stroke-dasharray:9 5} .dim{stroke:#263238;stroke-width:1.5;fill:none}</style>
|
||||
<defs><marker id="arrow" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M8 0L0 4L8 8" fill="#263238"/></marker></defs>
|
||||
<rect width="900" height="350" rx="10" fill="white"/>
|
||||
<text class="title" x="30" y="38">The dimension follows its paper-space centerline</text>
|
||||
<rect x="30" y="65" width="400" height="220" fill="#f5f8fa" stroke="#c3ced5"/>
|
||||
<rect x="470" y="65" width="400" height="220" fill="#f5f8fa" stroke="#c3ced5"/>
|
||||
<text class="label" x="48" y="93">Before</text><text class="label" x="488" y="93">Centerline moved 1 paper unit right</text>
|
||||
<path class="model" d="M85 160H385M525 160H825"/>
|
||||
<circle cx="385" cy="160" r="5" fill="#1479b8"/><circle cx="825" cy="160" r="5" fill="#1479b8"/>
|
||||
<path class="paper" d="M235 113V257M705 113V257"/>
|
||||
<path class="dim" d="M385 171V244M825 171V244"/>
|
||||
<path class="dim" d="M235 232H385" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
||||
<path class="dim" d="M705 232H825" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
||||
<rect x="291" y="217" width="38" height="28" fill="#f5f8fa"/><rect x="746" y="217" width="38" height="28" fill="#f5f8fa"/>
|
||||
<text class="label" x="310" y="237" text-anchor="middle">50</text><text class="label" x="765" y="237" text-anchor="middle">40</text>
|
||||
<text class="label" x="30" y="316">The model endpoint stays fixed. The dimension updates from 50 to 40.</text>
|
||||
<text class="small" x="30" y="340">Association schematic; viewport 1:10, dimension style unit factor = 1, DIMASSOC = 2.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="350" viewBox="0 0 900 350">
|
||||
<style>text{font-family:Arial,sans-serif;fill:#263238} .title{font-size:22px;font-weight:600} .label{font-size:16px} .small{font-size:13px;fill:#54616a} .model{stroke:#1479b8;stroke-width:4;fill:none} .paper{stroke:#a94610;stroke-width:2;stroke-dasharray:9 5} .dim{stroke:#263238;stroke-width:1.5;fill:none}</style>
|
||||
<defs><marker id="arrow" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M8 0L0 4L8 8" fill="#263238"/></marker></defs>
|
||||
<rect width="900" height="350" rx="10" fill="white"/>
|
||||
<text class="title" x="30" y="38">Dimension from a paper centerline to a model endpoint</text>
|
||||
<rect x="30" y="65" width="430" height="245" fill="#f5f8fa" stroke="#c3ced5"/>
|
||||
<text class="small" x="45" y="89">Viewport 1:10</text>
|
||||
<path class="model" d="M100 160H400"/>
|
||||
<circle cx="400" cy="160" r="5" fill="#1479b8"/>
|
||||
<path class="paper" d="M250 110V260"/>
|
||||
<text class="small" x="100" y="144">Model geometry</text>
|
||||
<path class="dim" d="M400 171V244"/>
|
||||
<path class="dim" d="M250 232H400" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
||||
<rect x="306" y="217" width="38" height="28" fill="#f5f8fa"/>
|
||||
<text class="label" x="325" y="237" text-anchor="middle">50</text>
|
||||
<text class="small" x="45" y="290">Brown dashed line: paper-space centerline</text>
|
||||
<text class="label" x="500" y="116">5 paper units between the picked points</text>
|
||||
<text class="label" x="500" y="155">5 × 10 = 50 model units</text>
|
||||
<text class="label" x="500" y="206">The viewport supplies the length scale.</text>
|
||||
<text class="label" x="500" y="240">Either pick order gives the same value.</text>
|
||||
<text class="small" x="30" y="336">Measurement schematic; dimension style unit factor = 1.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,74 @@
|
||||
# Dimensions through layout viewports
|
||||
|
||||
In paper space, snap to model geometry displayed through a planar, orthographic
|
||||
viewport. `DIMLINEAR`, `DIMALIGNED`, `DIMANGULAR`, `DIMRADIUS`, and `DIMDIAMETER`
|
||||
also support their usual object-pick modes through these viewports.
|
||||
|
||||
## Measurement
|
||||
|
||||
- Model geometry supplies the viewport scale. A 100-unit model length displayed
|
||||
at 1:10 measures 100, although it occupies 10 paper units.
|
||||
- A paper-space centerline or free sheet point can be combined with model
|
||||
geometry through one viewport, in either pick order. The paper-space span is
|
||||
measured using that viewport's scale.
|
||||
- Sheet-only dimensions measure paper units, even inside a viewport rectangle.
|
||||
- Model picks from a second viewport are rejected without advancing the command.
|
||||
Dimension-line and text placement do not change the measurement scale.
|
||||
- The current dimension style's unit conversion is preserved. Angular dimensions
|
||||
do not use length compensation. `DIMASSOC=0` explodes the compensated dimension.
|
||||
|
||||
Snapping and object picking respect visibility, clipping, and viewport draw order.
|
||||
Paper geometry has priority in object picks. Ordinary selection uses the active
|
||||
drawing space. Object picks reuse the resident interaction index and resolve
|
||||
nested block transforms.
|
||||
|
||||

|
||||
|
||||
## Saved scale
|
||||
|
||||
Length dimensions store paper-space definition points and a negative `DIMLFAC`
|
||||
override: user unit factor multiplied by `1 / viewport scale`.
|
||||
`OCS_VIEWPORT_MEASUREMENT` XData stores version `1` (integer16), the positive user
|
||||
factor (real), and viewport compensation (real). Keeping the factors separate
|
||||
allows compensation to change without losing the user's unit conversion.
|
||||
Both the override and XData survive DXF/DWG saves and undo/redo.
|
||||
|
||||
## Associations
|
||||
|
||||
With `DIMASSOC=2`, acquired features retain their viewport, block-instance path,
|
||||
source entity, and snap feature. Intersections retain both source chains.
|
||||
Source edits and viewport changes update the dimension and its compensation,
|
||||
preserving style overrides and user-positioned text on the sheet.
|
||||
|
||||
A sheet centerline stays on the sheet; a model endpoint follows its viewport.
|
||||
Moving the viewport therefore changes the distance between them. Undo/redo and
|
||||
DXF/DWG saves preserve the references.
|
||||
|
||||
Properties shows association status. If a required reference is broken or cannot
|
||||
be resolved, the entire dimension keeps its last valid geometry and reference
|
||||
records. A successful refresh regenerates its displayed and exported geometry.
|
||||
|
||||

|
||||
|
||||
## Limits
|
||||
|
||||
Unsupported cases preserve saved dimensions:
|
||||
|
||||
- Perspective or oblique views; missing or malformed reference chains.
|
||||
- Array instances without an addressable instance index, nesting beyond eight
|
||||
levels, and radial measurements of nonuniformly scaled circles or arcs.
|
||||
- Intersections without two identifiable source chains inside a batched block.
|
||||
- Ambiguous imported endpoint, midpoint, or quadrant markers. A stored point can
|
||||
validate a feature; it cannot establish an otherwise unknown identity.
|
||||
- Tangency without a verified solution, or two perpendicular/tangent references
|
||||
requiring a coupled solve.
|
||||
- Imported viewport scaling without OCS scale metadata or
|
||||
`ACAD_DIMASSOC_CALC_DIMLFAC` identifying the saved compensation.
|
||||
|
||||
## Tests
|
||||
|
||||
Run `cargo test --locked --lib viewport_dimension`. Tests cover the five commands,
|
||||
mixed sheet/model input, styles, clipping, object picks, source/viewport edits,
|
||||
snap features, nested paths, intersections, preserved overrides, undo/redo, and
|
||||
DXF/DWG persistence. The synthetic association fixture is in
|
||||
`tests/fixtures/dim_assoc/`.
|
||||