diff --git a/docs/superpowers/specs/2026-06-03-orcaslicer-file-open-method-design.md b/docs/superpowers/specs/2026-06-03-orcaslicer-file-open-method-design.md new file mode 100644 index 0000000000..39a06a160d --- /dev/null +++ b/docs/superpowers/specs/2026-06-03-orcaslicer-file-open-method-design.md @@ -0,0 +1,117 @@ +# Design — `file.open` automation method (runtime model loading) + +**Date:** 2026-06-03 +**Branch:** `feature/automation` +**Status:** Approved for implementation + +--- + +## Problem + +Today a model can be loaded into OrcaSlicer **only at process launch**: the model path +is passed as a CLI positional arg and OrcaSlicer's normal startup file-loading ingests +it. The JSON-RPC automation protocol has **no** load method, so swapping or adding a +model in an already-running instance requires a fresh process launch. + +Driving the native File→Import dialog via `input.click` is not a viable substitute: the +OS file picker is not a `wxWindow`, so it never appears in the `tree.dump` hierarchy +(`WxUiBackend::dump_tree` walks `wxGetApp().mainframe` children only), and `input.click` +can only target nodes resolved from that tree (no raw-coordinate click). Blind typing via +`input.type` is mechanically possible but unobservable: the native picker is not a +`wxDialog`, so `app.state().modal_dialog` and `sync.wait_for` cannot gate on it, leaving +only sleep-and-hope timing. A direct API method is the clean fix. + +## Goal + +Add a `file.open` JSON-RPC method that loads one or more files into a running instance by +calling `Plater::load_files(...)` directly on the GUI thread. Out of scope: any +dialog-driving mechanism (intercept hook or true OS-level drive) — explicitly deferred. + +--- + +## Protocol + +- **Method:** `file.open` +- **Params:** `{ "paths": ["C:/abs/a.stl", ...] }` + - A bare string is also accepted: `{ "paths": "C:/abs/a.stl" }`. + - Paths must be **absolute**. The server reads them from the host filesystem + (client/server are localhost-only). +- **Result:** `{ "ok": true, "loaded": }` + - `count` is `load_files(...).size()` — the number of objects added to the scene. +- **Errors:** + | Code | Constant | Condition | + |------|----------|-----------| + | 1002 | `kInvalidParams` | `paths` missing/empty, or a non-string entry | + | 1004 | `kErrGuiBusy` | GUI-thread marshal timed out (`m_gui_timeout_ms`) | + | 1007 | `kErrLoadFailed` | `load_files` returned empty / threw (not found, parse error, unsupported format) — **new code** | + +## Semantics — synchronous + +`Plater::load_files` runs and completes on the GUI thread. The backend marshals via the +existing `run_on_gui(m_gui_timeout_ms, …)` helper and returns only after the load +finishes. Consequently, when `file.open` returns `ok:true`, `app.state().project_loaded` +is already `true` — there is no polling race. + +Rejected alternative — async "fire-and-poll-`project_loaded`": adds client complexity and +loses a definitive per-call error result, with no benefit since loading is synchronous. + +**Caveat:** an extremely large model could exceed `m_gui_timeout_ms` and surface as +`1004 kErrGuiBusy`. Documented; not mitigated in v1. + +## Load strategy (v1 minimal) + +Pass the default `LoadStrategy::LoadModel | LoadStrategy::LoadConfig` (identical to +drag-drop / `Plater::load_files`'s default) with `ask_multi = false`. This already routes +`.3mf` files as projects and meshes as models based on file content, so **no `as_project` +flag is needed in v1**. A future `{ "as_project": bool }` flag remains possible but is not +implemented now. + +--- + +## Components / files to touch + +Follows the existing `screenshot_window` / `app_state` method pattern. + +1. **`src/slic3r/GUI/Automation/IUiBackend.hpp`** — add pure-virtual + `int open_files(const std::vector& paths)` returning the loaded count, + throwing `AutomationError` on failure. Header stays wx-free (no `LoadStrategy` leak). +2. **`src/slic3r/GUI/Automation/WxUiBackend.{hpp,cpp}`** — implement `open_files`: + `run_on_gui(m_gui_timeout_ms, …)` → `wxGetApp().plater()->load_files(paths, default_strategy, false)`; + throw `kErrLoadFailed` if the returned vector is empty. +3. **`src/slic3r/GUI/Automation/JsonRpcDispatcher.{hpp,cpp}`** — + - add `constexpr int kErrLoadFailed = 1007;` + - declare + define `m_file_open(params)` (param parsing/validation; accept string or + array; require ≥1 non-empty string path) + - add dispatch route `if (method == "file.open") return make_result(id, m_file_open(params));` + - add `"file.open"` to the capabilities array in `m_version`. +4. **`tests/automation/MockUiBackend.hpp`** — `open_files` override recording the paths + vector + a configurable return-count (and a throw/fail knob). +5. **`tests/automation/test_dispatcher.cpp`** — Catch2 v2 tests: + - array of paths → routes to backend, returns `loaded` count + - bare-string path → normalized to one path + - missing/empty `paths` → `1002` + - backend load failure → `1007` + - `automation.version` capabilities array includes `"file.open"` +6. **`tools/automation/orca_automation.py`** — `open(self, paths)` wrapper (normalize + `str` → `[str]`, send `file.open`). +7. **`tools/automation/example_slice.py`** — launch **without** a model arg, then + `orca.open([model])`, then wait for `project_loaded`. +8. **`doc/automation.md`** — document method (params/result/errors), add to the + capabilities list, method index, and error table (`1007`). + +--- + +## Testing / verification + +- **Build (Windows):** `cmake --build . --config RelWithDebInfo --target ALL_BUILD -- -m`. +- **Unit:** `automation` Catch2 suite green including new tests (≈31 → ≈34 cases). +- **Manual:** launch with `--automation-server` and **no** model arg → call `file.open` + → confirm `app.state().project_loaded` flips `true` and `screenshot.window` shows the + model. +- **Gating:** unchanged — the server only runs under `--automation-server`, so the method + is a no-op (unreachable) when automation is disabled. + +## Backward compatibility + +Additive only: a new method, a new error code, and a new capabilities entry. No change to +existing methods, profiles, project-file handling, or default behavior.