mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-06-18 18:04:40 +00:00
update doc
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,4 +45,5 @@ test.js
|
||||
.clangd
|
||||
internal_docs/
|
||||
*.flatpak
|
||||
/flatpak-repo/
|
||||
/flatpak-repo/
|
||||
*.pyc
|
||||
@@ -4,7 +4,8 @@ OrcaSlicer ships an **opt-in, localhost-only JSON-RPC server** that lets externa
|
||||
scripts introspect, drive, and screenshot the running OrcaSlicer GUI. It is built
|
||||
for end-to-end testing and automation: a script can enumerate the live widget
|
||||
tree, click buttons, type text, send keyboard shortcuts, wait for UI state, query
|
||||
high-level application state, and capture window images (the on-screen capture
|
||||
high-level application state, load models/projects into the running instance,
|
||||
switch the active view/tab, and capture window images (the on-screen capture
|
||||
includes the 3D viewport).
|
||||
|
||||
This document is the protocol reference. It describes activation, the transport,
|
||||
@@ -549,10 +550,14 @@ from orca_automation import OrcaClient
|
||||
orca = OrcaClient(port=13619)
|
||||
print(orca.version()) # {'version': '1.0.0', ...}
|
||||
|
||||
orca.select_view("prepare") # switch to the 3D editor
|
||||
orca.open(r"C:\models\part.stl") # load a model at runtime (synchronous)
|
||||
|
||||
orca.click({"id": "btn_slice"}) # start slicing the plate
|
||||
orca.wait_for({"id": "btn_export"}, # wait until slicing finishes
|
||||
state="enabled", timeout_ms=180000)
|
||||
|
||||
orca.select_view("preview") # switch to the sliced G-code preview
|
||||
png = orca.screenshot() # on-screen capture (incl. 3D view)
|
||||
with open("window.png", "wb") as f:
|
||||
f.write(png)
|
||||
|
||||
608
docs/superpowers/plans/2026-06-03-orcaslicer-file-open-method.md
Normal file
608
docs/superpowers/plans/2026-06-03-orcaslicer-file-open-method.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# `file.open` Automation Method Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a `file.open` JSON-RPC automation method that loads one or more files into an already-running OrcaSlicer instance by calling `Plater::load_files(...)` synchronously on the GUI thread.
|
||||
|
||||
**Architecture:** Follows the existing `screenshot.window` / `app.state` method pattern. A new pure-virtual `open_files(paths)` is added to the wx-free `IUiBackend` interface; `WxUiBackend` implements it via the existing `run_on_gui(...)` GUI-thread marshal calling `Plater::load_files`; the `JsonRpcDispatcher` gains a `file.open` route, a param-parsing helper, and a new `kErrLoadFailed = 1007` error code. The unit-testable surface (dispatcher + param validation + routing) is driven against `MockUiBackend`.
|
||||
|
||||
**Tech Stack:** C++17, nlohmann::json, Catch2 v2 (`catch_all.hpp` / `Catch2WithMain`), wxWidgets, CMake + Ninja Multi-Config. Python 3 reference client (stdlib only).
|
||||
|
||||
---
|
||||
|
||||
## Design-spec note (resolve before coding)
|
||||
|
||||
The design spec's error table reads `1002 | kInvalidParams | paths missing/empty…`, but in the codebase `kInvalidParams` is the standard JSON-RPC code **`-32602`**, while `1002` is `kErrNotActionable`. The spec's **Constant column (`kInvalidParams`) is authoritative** and matches every other param-validation path in the dispatcher (e.g. `m_input_type` throws `kInvalidParams` for a bad `text`). This plan therefore validates `file.open` params with **`kInvalidParams` (-32602)**, exactly like the existing handlers, and the tests assert `== kInvalidParams`. The literal "1002" in the spec is a typo; do not emit code 1002 for param errors.
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Change | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/slic3r/GUI/Automation/IUiBackend.hpp` | modify | Add pure-virtual `int open_files(paths)` to the backend abstraction (stays wx-free). |
|
||||
| `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp` | modify | Add `kErrLoadFailed = 1007` constant + `m_file_open` declaration. |
|
||||
| `src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp` | modify | Add `parse_paths` helper, `m_file_open` body, dispatch route, capabilities entry. |
|
||||
| `src/slic3r/GUI/Automation/WxUiBackend.hpp` | modify | Declare `open_files` override. |
|
||||
| `src/slic3r/GUI/Automation/WxUiBackend.cpp` | modify | Implement `open_files` via `run_on_gui` → `Plater::load_files`. |
|
||||
| `tests/automation/MockUiBackend.hpp` | modify | `open_files` override: record paths + return-count + fail knob. |
|
||||
| `tests/automation/test_dispatcher.cpp` | modify | Catch2 tests for routing, string/array, validation, failure, capabilities. |
|
||||
| `tools/automation/orca_automation.py` | modify | `open(self, paths)` client wrapper. |
|
||||
| `tools/automation/example_slice.py` | modify | Launch without a model arg, then `orca.open([model])`. |
|
||||
| `doc/automation.md` | modify | Document the method, capabilities, error `1007`. |
|
||||
|
||||
**Build/test layout:** Ninja Multi-Config in `build/`. The unit suite target is `automation_tests`; its sources (`tests/automation/CMakeLists.txt`) compile `JsonRpcDispatcher.cpp` + `MockUiBackend` but **not** `WxUiBackend.cpp`. So dispatcher/mock changes are fully unit-testable headlessly; `WxUiBackend.cpp` is verified by the full app build only.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Extend the backend abstraction (interface + mock + error code)
|
||||
|
||||
Adds the `open_files` contract so tests can be written. Adding a pure virtual to `IUiBackend` forces every implementation to provide it — in the unit-test target that is only `MockUiBackend`, so this task keeps the `automation_tests` build green.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/slic3r/GUI/Automation/IUiBackend.hpp`
|
||||
- Modify: `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp:19`
|
||||
- Modify: `tests/automation/MockUiBackend.hpp`
|
||||
|
||||
- [ ] **Step 1: Add the error constant**
|
||||
|
||||
In `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp`, after the existing `kErrDisabled` line (currently line 19), add:
|
||||
|
||||
```cpp
|
||||
constexpr int kErrDisabled = 1006;
|
||||
constexpr int kErrLoadFailed = 1007; // file.open: load_files returned empty / threw
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the pure-virtual `open_files` to the interface**
|
||||
|
||||
In `src/slic3r/GUI/Automation/IUiBackend.hpp`, inside `class IUiBackend`, immediately after the `screenshot_window` pure virtual (currently line 97), add:
|
||||
|
||||
```cpp
|
||||
// Load one or more files (absolute paths) into the running instance on the GUI
|
||||
// thread. Returns the number of objects added to the scene (load_files(...).size()).
|
||||
// Throws AutomationError(kErrLoadFailed) when nothing loads. Header stays wx-free:
|
||||
// the concrete LoadStrategy is chosen inside WxUiBackend, not exposed here.
|
||||
virtual int open_files(const std::vector<std::string>& paths) = 0;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Implement `open_files` in the mock with record + knobs**
|
||||
|
||||
In `tests/automation/MockUiBackend.hpp`: add an include for the error constant near the top (after the `IUiBackend.hpp` include on line 2):
|
||||
|
||||
```cpp
|
||||
#include "slic3r/GUI/Automation/IUiBackend.hpp"
|
||||
#include "slic3r/GUI/Automation/JsonRpcDispatcher.hpp" // kErrLoadFailed
|
||||
```
|
||||
|
||||
Add recorded-call + canned-output members. After the `screenshot_window_count` recorded field (line 20) add:
|
||||
|
||||
```cpp
|
||||
int screenshot_window_count = 0;
|
||||
std::vector<std::vector<std::string>> opened_paths; // paths of each open_files()
|
||||
```
|
||||
|
||||
After the `click_result` canned field (line 26) add:
|
||||
|
||||
```cpp
|
||||
bool click_result = true;
|
||||
int open_return_count = 0; // value open_files() returns
|
||||
bool open_should_fail = false; // when true, open_files() throws kErrLoadFailed
|
||||
```
|
||||
|
||||
Add the override next to the other overrides, after `screenshot_window` (lines 49-51):
|
||||
|
||||
```cpp
|
||||
PngImage screenshot_window(const UiNode*) override {
|
||||
++screenshot_window_count; return canned_png;
|
||||
}
|
||||
int open_files(const std::vector<std::string>& paths) override {
|
||||
opened_paths.push_back(paths);
|
||||
if (open_should_fail)
|
||||
throw AutomationError(kErrLoadFailed, "mock load failed");
|
||||
return open_return_count;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build the unit-test target to confirm it still compiles**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target automation_tests`
|
||||
Expected: build succeeds (the new pure virtual is satisfied by the mock; no behavior change yet).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/GUI/Automation/IUiBackend.hpp src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp tests/automation/MockUiBackend.hpp
|
||||
git commit -m "feat(automation): add open_files to backend interface + kErrLoadFailed (1007)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `file.open` dispatcher handler (parse, route, validate, fail)
|
||||
|
||||
Implements the full JSON-RPC handler against the mock: param parsing (string or array), validation, routing to `open_files`, and `kErrLoadFailed` propagation.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp:49` (declaration)
|
||||
- Modify: `src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp`
|
||||
- Test: `tests/automation/test_dispatcher.cpp`
|
||||
|
||||
- [ ] **Step 1: Write the failing happy-path test (array of paths)**
|
||||
|
||||
Append to `tests/automation/test_dispatcher.cpp`:
|
||||
|
||||
```cpp
|
||||
TEST_CASE("file.open with an array of paths routes to backend", "[automation][rpc]") {
|
||||
MockUiBackend mock;
|
||||
mock.open_return_count = 3;
|
||||
JsonRpcDispatcher d(mock);
|
||||
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","file.open"},
|
||||
{"params",{{"paths", json::array({"C:/abs/a.stl","C:/abs/b.stl"})}}}});
|
||||
CHECK(resp.at("result").at("ok") == true);
|
||||
CHECK(resp.at("result").at("loaded") == 3);
|
||||
REQUIRE(mock.opened_paths.size() == 1);
|
||||
REQUIRE(mock.opened_paths[0].size() == 2);
|
||||
CHECK(mock.opened_paths[0][0] == "C:/abs/a.stl");
|
||||
CHECK(mock.opened_paths[0][1] == "C:/abs/b.stl");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "file.open with an array of paths routes to backend"`
|
||||
Expected: FAIL — `file.open` is an unknown method, so the response carries `error.code == -32601` and has no `result` (the `resp.at("result")` access throws). (If the exe path differs on your machine, locate it with `find build -iname automation_tests.exe`.)
|
||||
|
||||
- [ ] **Step 3: Declare the handler**
|
||||
|
||||
In `src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp`, after the `m_screenshot_window` declaration (currently line 49) add:
|
||||
|
||||
```cpp
|
||||
nlohmann::json m_screenshot_window(const nlohmann::json& params);
|
||||
nlohmann::json m_file_open(const nlohmann::json& params);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the `parse_paths` helper and `m_file_open` body**
|
||||
|
||||
In `src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp`, add a `parse_paths` helper. Place it in the anonymous namespace that also holds `parse_keys` — insert it right before that namespace's closing `} // namespace` (currently line 130):
|
||||
|
||||
```cpp
|
||||
// "paths" may be a single string ("C:/a.stl") or an array of strings. Returns the
|
||||
// non-empty absolute paths; throws kInvalidParams when paths is missing, not a
|
||||
// string/array, contains a non-string entry, or yields no non-empty path.
|
||||
std::vector<std::string> parse_paths(const nlohmann::json& params) {
|
||||
if (!params.is_object() || !params.contains("paths"))
|
||||
throw AutomationError(kInvalidParams, "file.open requires 'paths'");
|
||||
const auto& p = params.at("paths");
|
||||
std::vector<std::string> out;
|
||||
if (p.is_string()) {
|
||||
out.push_back(p.get<std::string>());
|
||||
} else if (p.is_array()) {
|
||||
for (const auto& e : p) {
|
||||
if (!e.is_string())
|
||||
throw AutomationError(kInvalidParams, "'paths' entries must be strings");
|
||||
out.push_back(e.get<std::string>());
|
||||
}
|
||||
} else {
|
||||
throw AutomationError(kInvalidParams, "'paths' must be a string or array");
|
||||
}
|
||||
out.erase(std::remove_if(out.begin(), out.end(),
|
||||
[](const std::string& s) { return s.empty(); }),
|
||||
out.end());
|
||||
if (out.empty())
|
||||
throw AutomationError(kInvalidParams, "'paths' is empty");
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
(`<algorithm>` for `std::remove_if` is already included at the top of the file, line 4.)
|
||||
|
||||
Add the handler body next to the other handlers. After `m_screenshot_window` (currently ends line 343, just before the final `}}} // namespace`), add:
|
||||
|
||||
```cpp
|
||||
nlohmann::json JsonRpcDispatcher::m_file_open(const nlohmann::json& params) {
|
||||
const std::vector<std::string> paths = parse_paths(params);
|
||||
const int loaded = m_backend.open_files(paths);
|
||||
return { {"ok", true}, {"loaded", loaded} };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the dispatch route**
|
||||
|
||||
In `JsonRpcDispatcher::dispatch`, after the `screenshot.window` route (currently line 195) add:
|
||||
|
||||
```cpp
|
||||
if (method == "screenshot.window") return make_result(id, m_screenshot_window(params));
|
||||
if (method == "file.open") return make_result(id, m_file_open(params));
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the happy-path test to verify it passes**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "file.open with an array of paths routes to backend"`
|
||||
Expected: PASS — 1 test case, all assertions passed.
|
||||
|
||||
- [ ] **Step 7: Add the remaining handler tests (string, validation, failure)**
|
||||
|
||||
Append to `tests/automation/test_dispatcher.cpp`:
|
||||
|
||||
```cpp
|
||||
TEST_CASE("file.open accepts a bare string path", "[automation][rpc]") {
|
||||
MockUiBackend mock;
|
||||
mock.open_return_count = 1;
|
||||
JsonRpcDispatcher d(mock);
|
||||
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",2},{"method","file.open"},
|
||||
{"params",{{"paths","C:/abs/a.stl"}}}});
|
||||
CHECK(resp.at("result").at("loaded") == 1);
|
||||
REQUIRE(mock.opened_paths.size() == 1);
|
||||
REQUIRE(mock.opened_paths[0].size() == 1);
|
||||
CHECK(mock.opened_paths[0][0] == "C:/abs/a.stl");
|
||||
}
|
||||
|
||||
TEST_CASE("file.open with missing paths -> invalid params", "[automation][rpc]") {
|
||||
MockUiBackend mock;
|
||||
JsonRpcDispatcher d(mock);
|
||||
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",3},{"method","file.open"},
|
||||
{"params", json::object()}});
|
||||
CHECK(resp.at("error").at("code") == kInvalidParams);
|
||||
CHECK(mock.opened_paths.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file.open with empty paths array -> invalid params", "[automation][rpc]") {
|
||||
MockUiBackend mock;
|
||||
JsonRpcDispatcher d(mock);
|
||||
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",4},{"method","file.open"},
|
||||
{"params",{{"paths", json::array()}}}});
|
||||
CHECK(resp.at("error").at("code") == kInvalidParams);
|
||||
CHECK(mock.opened_paths.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file.open with a non-string entry -> invalid params", "[automation][rpc]") {
|
||||
MockUiBackend mock;
|
||||
JsonRpcDispatcher d(mock);
|
||||
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",5},{"method","file.open"},
|
||||
{"params",{{"paths", json::array({"C:/a.stl", 42})}}}});
|
||||
CHECK(resp.at("error").at("code") == kInvalidParams);
|
||||
CHECK(mock.opened_paths.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file.open backend load failure -> 1007", "[automation][rpc]") {
|
||||
MockUiBackend mock;
|
||||
mock.open_should_fail = true;
|
||||
JsonRpcDispatcher d(mock);
|
||||
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",6},{"method","file.open"},
|
||||
{"params",{{"paths","C:/abs/a.stl"}}}});
|
||||
CHECK(resp.at("error").at("code") == kErrLoadFailed);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Run all file.open tests to verify they pass**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "file.open*"`
|
||||
Expected: PASS — 6 test cases, all assertions passed.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp tests/automation/test_dispatcher.cpp
|
||||
git commit -m "feat(automation): add file.open dispatcher handler with validation + tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Advertise `file.open` in `automation.version` capabilities
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp:166-172`
|
||||
- Test: `tests/automation/test_dispatcher.cpp`
|
||||
|
||||
- [ ] **Step 1: Write the failing capabilities test**
|
||||
|
||||
Append to `tests/automation/test_dispatcher.cpp`:
|
||||
|
||||
```cpp
|
||||
TEST_CASE("automation.version capabilities include file.open", "[automation][rpc]") {
|
||||
MockUiBackend mock;
|
||||
JsonRpcDispatcher d(mock);
|
||||
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","automation.version"}});
|
||||
const auto& caps = resp.at("result").at("capabilities");
|
||||
bool found = false;
|
||||
for (const auto& c : caps) if (c == "file.open") found = true;
|
||||
CHECK(found);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "automation.version capabilities include file.open"`
|
||||
Expected: FAIL — `CHECK(found)` is false; `file.open` is not yet in the capabilities array.
|
||||
|
||||
- [ ] **Step 3: Add `file.open` to the capabilities array**
|
||||
|
||||
In `JsonRpcDispatcher::m_version` (currently lines 166-172), add `"file.open"` to the array:
|
||||
|
||||
```cpp
|
||||
nlohmann::json JsonRpcDispatcher::m_version(const nlohmann::json&) {
|
||||
return { {"version", kAutomationVersion},
|
||||
{"protocol", "2.0"},
|
||||
{"capabilities", nlohmann::json::array({
|
||||
"tree.dump","tree.find","widget.get","input.click","input.type",
|
||||
"input.key","sync.wait_for","app.state","screenshot.window",
|
||||
"file.open" })} };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe "automation.version capabilities include file.open"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Run the whole automation suite to confirm no regressions**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe --order rand --warn NoAssertions`
|
||||
Expected: PASS — all cases green (the pre-existing ~32 plus the 7 new `file.open`/capabilities cases ≈ 39).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp tests/automation/test_dispatcher.cpp
|
||||
git commit -m "feat(automation): advertise file.open in automation.version capabilities"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Implement `WxUiBackend::open_files` (real GUI-thread load)
|
||||
|
||||
Not covered by the headless unit suite (`WxUiBackend.cpp` is excluded from `automation_tests`); verified by the full app build + the manual runtime check in Task 8.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/slic3r/GUI/Automation/WxUiBackend.hpp:21`
|
||||
- Modify: `src/slic3r/GUI/Automation/WxUiBackend.cpp`
|
||||
|
||||
- [ ] **Step 1: Declare the override**
|
||||
|
||||
In `src/slic3r/GUI/Automation/WxUiBackend.hpp`, after the `screenshot_window` declaration (line 21) add:
|
||||
|
||||
```cpp
|
||||
PngImage screenshot_window(const UiNode* target) override;
|
||||
int open_files(const std::vector<std::string>& paths) override;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement `open_files`**
|
||||
|
||||
In `src/slic3r/GUI/Automation/WxUiBackend.cpp`, add the implementation just before the final `}}} // namespace Slic3r::GUI::Automation` (currently line 306):
|
||||
|
||||
```cpp
|
||||
int WxUiBackend::open_files(const std::vector<std::string>& paths) {
|
||||
return run_on_gui(m_gui_timeout_ms, [&]() -> int {
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
throw AutomationError(kErrLoadFailed, "no plater to load into");
|
||||
// Default strategy matches drag-drop / Plater::load_files's own default: it
|
||||
// routes .3mf as a project and meshes as models based on file content, so no
|
||||
// as_project flag is needed in v1. ask_multi=false: never prompt.
|
||||
const LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig;
|
||||
std::vector<size_t> loaded;
|
||||
try {
|
||||
loaded = plater->load_files(paths, strategy, /*ask_multi=*/false);
|
||||
} catch (const std::exception& e) {
|
||||
throw AutomationError(kErrLoadFailed,
|
||||
std::string("load_files failed: ") + e.what());
|
||||
}
|
||||
if (loaded.empty())
|
||||
throw AutomationError(kErrLoadFailed, "load_files loaded nothing");
|
||||
return static_cast<int>(loaded.size());
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Notes for the implementer:
|
||||
- `LoadStrategy` and its `operator|` (namespace `Slic3r`, from `libslic3r/Format/bbs_3mf.hpp`) are already in scope: `WxUiBackend.cpp` includes `Plater.hpp` (line 7), which transitively pulls in the enum, and this translation unit lives in `Slic3r::GUI::Automation` so unqualified `LoadStrategy` resolves via the enclosing `Slic3r` namespace. No new include is required.
|
||||
- `Plater::load_files(const std::vector<std::string>&, LoadStrategy, bool)` is the existing string overload (`Plater.hpp:379`) — no `boost::filesystem::path` conversion needed.
|
||||
- `kErrLoadFailed` comes from `JsonRpcDispatcher.hpp`, already included at line 4.
|
||||
- An `AutomationError` thrown inside the `run_on_gui` lambda is captured by the helper's `set_exception` and rethrown from `fut.get()`, so the 1007 code propagates to the dispatcher unchanged.
|
||||
|
||||
- [ ] **Step 3: Build the full app to verify it compiles and links**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target OrcaSlicer`
|
||||
Expected: build succeeds (no missing-symbol / pure-virtual errors; `WxUiBackend` is now concrete).
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/GUI/Automation/WxUiBackend.hpp src/slic3r/GUI/Automation/WxUiBackend.cpp
|
||||
git commit -m "feat(automation): implement WxUiBackend::open_files via Plater::load_files"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Python client wrapper `OrcaClient.open`
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/automation/orca_automation.py:80-82`
|
||||
|
||||
- [ ] **Step 1: Add the `open` method**
|
||||
|
||||
In `tools/automation/orca_automation.py`, after the `key` method (ends line 82), add:
|
||||
|
||||
```python
|
||||
def key(self, keys) -> dict:
|
||||
# keys: "ctrl+s" or ["ctrl", "s"]
|
||||
return self._call("input.key", {"keys": keys})
|
||||
|
||||
def open(self, paths) -> dict:
|
||||
"""Load one or more files into the running instance at runtime.
|
||||
|
||||
`paths` is a single absolute path string or a list of them. Paths are read
|
||||
from the host filesystem by the server (localhost-only). Returns
|
||||
{"ok": True, "loaded": <count>}. Raises OrcaError 1007 on load failure."""
|
||||
if isinstance(paths, str):
|
||||
paths = [paths]
|
||||
return self._call("file.open", {"paths": list(paths)})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Smoke-test the wrapper's normalization offline (no server needed)**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
python -c "import sys; sys.path.insert(0, 'tools/automation'); import orca_automation as m; c = m.OrcaClient.__new__(m.OrcaClient); c._call = lambda meth, params=None: (meth, params); print(c.open('C:/a.stl')); print(c.open(['C:/a.stl','C:/b.stl']))"
|
||||
```
|
||||
Expected output:
|
||||
```
|
||||
('file.open', {'paths': ['C:/a.stl']})
|
||||
('file.open', {'paths': ['C:/a.stl', 'C:/b.stl']})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tools/automation/orca_automation.py
|
||||
git commit -m "feat(automation): add OrcaClient.open() wrapper for file.open"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Update `example_slice.py` to load at runtime via `file.open`
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/automation/example_slice.py:26-52`
|
||||
|
||||
- [ ] **Step 1: Launch without the model arg, then call `open`**
|
||||
|
||||
In `tools/automation/example_slice.py`, change the `subprocess.Popen` call (lines 26-31) to drop the trailing model positional:
|
||||
|
||||
```python
|
||||
proc = subprocess.Popen([
|
||||
args.orca,
|
||||
"--automation-server",
|
||||
f"--automation-server-port={args.port}",
|
||||
])
|
||||
```
|
||||
|
||||
Then replace the project-load wait block (currently lines 46-51) so the model is loaded at runtime via `file.open` instead of relying on a launch-time positional:
|
||||
|
||||
```python
|
||||
# Load the model into the already-running instance, then wait until the
|
||||
# project reports loaded. file.open is synchronous, so project_loaded is
|
||||
# already true on return; the wait is a belt-and-suspenders guard.
|
||||
orca.open([args.model])
|
||||
deadline = time.time() + 30
|
||||
while time.time() < deadline:
|
||||
if orca.app_state().get("project_loaded"):
|
||||
break
|
||||
time.sleep(0.5)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Byte-compile the script to confirm no syntax errors**
|
||||
|
||||
Run: `python -m py_compile tools/automation/example_slice.py`
|
||||
Expected: no output, exit code 0.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tools/automation/example_slice.py
|
||||
git commit -m "docs(automation): example_slice.py loads model at runtime via file.open"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Document `file.open` in `doc/automation.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `doc/automation.md` (capabilities example §4 line 111-114; new method subsection after `screenshot.window`; error table §7)
|
||||
|
||||
- [ ] **Step 1: Add `file.open` to the capabilities example**
|
||||
|
||||
In `doc/automation.md`, update the `automation.version` result example (lines 111-114) to include `file.open`:
|
||||
|
||||
```json
|
||||
"capabilities": [
|
||||
"tree.dump", "tree.find", "widget.get", "input.click", "input.type",
|
||||
"input.key", "sync.wait_for", "app.state", "screenshot.window", "file.open"
|
||||
]
|
||||
```
|
||||
|
||||
The §4 prose count is already written for this: "There are 11 methods … the 10 callable feature methods" now matches exactly (10 capability entries + `automation.version` = 11). Leave that sentence unchanged.
|
||||
|
||||
- [ ] **Step 2: Add the `file.open` method subsection**
|
||||
|
||||
In `doc/automation.md`, immediately after the `screenshot.window` method subsection (it ends just before the `---` on line 303) and before that `---`, insert:
|
||||
|
||||
```markdown
|
||||
### `file.open`
|
||||
|
||||
Load one or more files into the **already-running** instance at runtime, by calling
|
||||
`Plater::load_files(...)` directly on the GUI thread. This is the supported way to add
|
||||
or swap a model without relaunching the process. Loading is **synchronous**: when the
|
||||
call returns `ok: true`, `app.state().project_loaded` is already `true` (no polling
|
||||
race).
|
||||
|
||||
**Params:**
|
||||
|
||||
| Param | Type | Required | Meaning |
|
||||
|---|---|---|---|
|
||||
| `paths` | string or array of strings | yes | One or more **absolute** file paths. A bare string is accepted and treated as a one-element list. Paths are read from the **host (server) filesystem** — client and server are localhost-only. |
|
||||
|
||||
`.3mf` files are routed as projects and meshes as models automatically, based on file
|
||||
content (the same default strategy as drag-drop); there is no `as_project` flag in v1.
|
||||
|
||||
**Result:** `{ "ok": true, "loaded": <int> }`, where `loaded` is the number of objects
|
||||
added to the scene (`load_files(...).size()`).
|
||||
|
||||
**Errors:**
|
||||
|
||||
- `-32602` (invalid params) — `paths` is missing, is not a string/array, contains a
|
||||
non-string entry, or yields no non-empty path.
|
||||
- `1007` (load failed) — `load_files` returned empty or threw (file not found, parse
|
||||
error, or unsupported format).
|
||||
- `1004` (GUI busy) — the GUI-thread marshal timed out. An extremely large model can
|
||||
exceed the marshal timeout and surface here; documented, not mitigated in v1.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the `1007` row to the error-code table**
|
||||
|
||||
In `doc/automation.md` §7, in the application-specific codes table, after the `1006` row (line 395) add:
|
||||
|
||||
```markdown
|
||||
| `1006` | Disabled. |
|
||||
| `1007` | Load failed — `file.open`'s `load_files` returned empty or threw (not found, parse error, unsupported format). |
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add doc/automation.md
|
||||
git commit -m "docs(automation): document file.open method and error 1007"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final verification
|
||||
|
||||
- [ ] **Step 1: Full automation unit suite green**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target automation_tests && build/tests/automation/RelWithDebInfo/automation_tests.exe --order rand --warn NoAssertions`
|
||||
Expected: PASS — all cases (pre-existing ~32 + 7 new) green, no `NoAssertions` warnings on the new cases.
|
||||
|
||||
- [ ] **Step 2: Full app builds**
|
||||
|
||||
Run: `cmake --build build --config RelWithDebInfo --target ALL_BUILD -- -m`
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 3: Manual runtime check (requires a display)**
|
||||
|
||||
Launch with `--automation-server` and **no** model arg, then from a Python shell:
|
||||
```python
|
||||
from orca_automation import OrcaClient
|
||||
orca = OrcaClient(port=13619)
|
||||
print(orca.open(["C:/abs/path/cube.stl"])) # -> {'ok': True, 'loaded': 1}
|
||||
print(orca.app_state()["project_loaded"]) # -> True
|
||||
open("window.png","wb").write(orca.screenshot()) # PNG shows the loaded model
|
||||
```
|
||||
Expected: `loaded >= 1`, `project_loaded == True`, screenshot shows the model.
|
||||
|
||||
- [ ] **Step 4: Gating check (automation OFF is a no-op)**
|
||||
|
||||
Confirm by reading: with no `--automation-server` flag, the server/backend/dispatcher are never constructed (`GUI_App.cpp` `start_automation_server()` early-return), so `file.open` is unreachable. No new hot-path cost beyond the existing single bool check. (See `doc/automation.md` §Verification — disabled-path audit; this feature adds no new gating surface.)
|
||||
|
||||
---
|
||||
|
||||
## Backward compatibility
|
||||
|
||||
Additive only: one new method (`file.open`), one new error code (`1007`), one new capabilities entry, and one new backend interface method. No existing method, profile, project-file handling, or default behavior changes. The method is reachable only when `--automation-server` is passed.
|
||||
@@ -41,7 +41,7 @@ dialog-driving mechanism (intercept hook or true OS-level drive) — explicitly
|
||||
- **Errors:**
|
||||
| Code | Constant | Condition |
|
||||
|------|----------|-----------|
|
||||
| 1002 | `kInvalidParams` | `paths` missing/empty, or a non-string entry |
|
||||
| -32602 | `kInvalidParams` | `paths` missing/empty, a non-string entry, or no non-empty path |
|
||||
| 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** |
|
||||
|
||||
@@ -89,7 +89,7 @@ Follows the existing `screenshot_window` / `app_state` method pattern.
|
||||
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`
|
||||
- missing/empty `paths` → `-32602` (`kInvalidParams`)
|
||||
- backend load failure → `1007`
|
||||
- `automation.version` capabilities array includes `"file.open"`
|
||||
6. **`tools/automation/orca_automation.py`** — `open(self, paths)` wrapper (normalize
|
||||
|
||||
Reference in New Issue
Block a user