diff --git a/docs/automation/README.md b/docs/automation/README.md index 1dcf4a90e..8293aec55 100644 --- a/docs/automation/README.md +++ b/docs/automation/README.md @@ -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 diff --git a/docs/automation/native.md b/docs/automation/native.md new file mode 100644 index 000000000..3d3c9f9a7 --- /dev/null +++ b/docs/automation/native.md @@ -0,0 +1,107 @@ +# Driving the native builds + +Every native build exposes the same control core through two entry points: the MCP +server described in the [README](README.md) (`OpenCADStudio --mcp`), and a headless +JSON-line channel meant for scripts and CI (`OpenCADStudio --serve`). The operations, +request envelope and replies are the same; this page covers what is specific to the +native side. The browser entry is described in [web.md](web.md). + +## Choosing a channel + +| | `--mcp` | `--serve` | +|---|---|---| +| Transport | MCP over stdio | one JSON request per line on stdin, one reply per line on stdout | +| Editor | starts the desktop editor and exposes the live document | no window; works on its own document | +| Needs a display | yes — on a headless machine `xvfb-run` works, on software rendering the viewport may show the GPU warning dialog | no | +| `measure`, `ocs_capture`, `set_properties`, interactive steps | available | `measure` and `capture` are not; the remaining operations are | +| Good for | an MCP client driving a real editor | batch jobs, CI, agents without a display | + +`OpenCADStudio --export IN OUT` converts a drawing in one shot, without any session. It +accepts `.dwg` and `.dxf` targets only. + +## Requirements + +The process needs a writable `HOME` (or `XDG_CONFIG_HOME`) — without one, `ocs_sessions` +answers `No user configuration directory` and nothing else works. + +## Startup dialogs + +A fresh profile opens two dialogs before any drawing exists, and both of them 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, so there is +no `document_id` to work with. 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. See +[web.md](web.md) for the same pattern on the browser side. + +## Targeting a document + +Reads and mutations target the active tab. `new` creates a document but does not move +the active tab, so pass the `document_id` from the reply explicitly — without it, reads +answer empty and it looks like nothing was created. `activate` switches the active tab. +A client connection to `--mcp` starts a new editor instance, so session state does not +carry over between connections. + +## Feeding a whole command line + +`run` takes one complete command line, including the answers to every prompt, and then +terminates the command as if Enter were pressed. A prompt cannot be continued on a +following line — each line is parsed as a fresh command line, so a command left waiting +is replaced by the next one (send `{"op":"cancel"}` if you want to drop it explicitly). + +`waiting_input` means input is still required — not that nothing happened. `SPLINE` and +`ARC`, for example, keep accepting further input even after the entity exists, and +`zoom_extents` reports it while the view has already changed. Send `{"op":"cancel"}` to +close the command when the line is done with it. + +Two reply fields describe what happened to the line: + +| Field | Meaning | +|---|---| +| `unconsumed` | Tokens that no prompt ever asked for, in order. `CIRCLE 0,0 5 9` reports `["9"]`, because the radius step ends the command; a fully consumed line reports `[]`. | +| `blocked_by` | What *this line* left open: `"command"`, `"text_editor"`, `"mtext_editor"`, `"modal:"`, or `null`. Set whenever `status` is `waiting_input`. | + +`TEXT 0,0 5 0 hello world` therefore creates one `Text` entity that holds +`hello world`, reports `completed` with `added: 1`, and leaves nothing unconsumed. The +content step runs in the in-place editor, which the feeder commits and then closes so +that the command does not stay armed for a second line. + +## 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`, `prompt` 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; separate `x`/`y` fields are rejected | +| 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` | +| the text of `TEXT`/`MTEXT` | `{"op":"action","name":"text_input","value":"hello"}` then `{"op":"action","name":"text_commit"}` | the command has already ended at that point and the editor owns the input | + +## Native-only operations + +| Operation | Device | +|---|---| +| `measure` | `ocs_read` with `op: "measure"` and `parameters.handles` returns kernel-computed length, area, bounds and mass properties. | +| `ocs_capture` | Returns the viewport or the whole window. Either an inline base64 image, or a PNG written under the temporary directory with the reply carrying its `path`. | +| `set_properties` | Mutates record properties. It requires `collection`, and colours are the serialised enum — `{"Index": 1}` or `"ByLayer"`; a colour name such as `"Red"` is rejected with `invalid_value`. | + +## Known gaps + +On the `--serve` path, `MTEXT`, `HATCH` and `POLYGON` report `completed` with +`added: 0`: they need interactive surfaces (a pattern choice, a boundary selection) that +a single command line has no tokens to fill. `TEXT` no longer belongs to that list.