mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-06-04 09:32:13 +03:00
Compare commits
11 Commits
feature/au
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0a47c61a2 | ||
|
|
a3e479af4e | ||
|
|
dc5d6b45de | ||
|
|
58a8722a69 | ||
|
|
dfe4b52822 | ||
|
|
9a053f15eb | ||
|
|
c467990724 | ||
|
|
757b6a5c46 | ||
|
|
6f011c9f6a | ||
|
|
ae16c76dd2 | ||
|
|
065540e48f |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,5 +45,4 @@ test.js
|
||||
.clangd
|
||||
internal_docs/
|
||||
*.flatpak
|
||||
/flatpak-repo/
|
||||
*.pyc
|
||||
/flatpak-repo/
|
||||
@@ -1,668 +0,0 @@
|
||||
# OrcaSlicer UI Automation Protocol (v1.0.0)
|
||||
|
||||
OrcaSlicer ships an **opt-in, localhost-only JSON-RPC server** that lets external
|
||||
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, 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,
|
||||
the JSON-RPC envelope, every method, the unified node shape, the target/locator
|
||||
model, error codes, the set of instrumented automation ids, ImGui specifics,
|
||||
platform caveats, a quick-start snippet, and planned future work.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview & activation
|
||||
|
||||
The automation server is **OFF by default**. It is enabled with two
|
||||
command-line flags:
|
||||
|
||||
| Flag | Meaning |
|
||||
|---|---|
|
||||
| `--automation-server` | Enable the automation server. |
|
||||
| `--automation-server-port=PORT` | Override the listening port. Optional; default is **13619**. |
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
OrcaSlicer --automation-server --automation-server-port=13619 model.stl
|
||||
```
|
||||
|
||||
The server binds to **`127.0.0.1` only** (the loopback interface). It is never
|
||||
exposed on an external network interface.
|
||||
|
||||
**Security note (v1):** there is **no authentication token** in v1. The localhost
|
||||
bind is the *only* security boundary. Any process able to run code on the machine
|
||||
can connect to the port and drive the GUI — including injecting mouse and keyboard
|
||||
input — while the server is enabled. The feature is intended for testing and
|
||||
automation environments, not for production or shared/multi-user machines.
|
||||
|
||||
When the server is enabled, OrcaSlicer emits a `warning`-level log line at startup
|
||||
to make the active input-injection surface obvious in logs, for example:
|
||||
|
||||
```
|
||||
UI automation server ENABLED ... input injection is active
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Transport
|
||||
|
||||
The server speaks **HTTP/1.1** over the loopback TCP socket:
|
||||
|
||||
| Request | Response |
|
||||
|---|---|
|
||||
| `POST /jsonrpc` with a JSON-RPC 2.0 request body | A JSON-RPC 2.0 response with `Content-Type: application/json`. |
|
||||
| `GET /` | A plain-text health page: `OrcaSlicer automation server v1.0.0` (`Content-Type: text/plain`). |
|
||||
| Anything else | HTTP `404 Not Found`. |
|
||||
|
||||
The server is **single-client / serialized** in v1: it handles one request at a
|
||||
time on its own dedicated I/O thread. Connections are not kept alive; each request
|
||||
is answered and the socket is closed. Clients should issue requests sequentially.
|
||||
|
||||
---
|
||||
|
||||
## 3. JSON-RPC envelope
|
||||
|
||||
The protocol follows **JSON-RPC 2.0**.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "jsonrpc": "2.0", "id": <id>, "method": "<method>", "params": { ... } }
|
||||
```
|
||||
|
||||
- `params` may be omitted; the server treats a missing `params` as an empty object.
|
||||
|
||||
**Success response:**
|
||||
|
||||
```json
|
||||
{ "jsonrpc": "2.0", "id": <id>, "result": { ... } }
|
||||
```
|
||||
|
||||
**Error response:**
|
||||
|
||||
```json
|
||||
{ "jsonrpc": "2.0", "id": <id>, "error": { "code": <int>, "message": "<string>" } }
|
||||
```
|
||||
|
||||
The request `id` is echoed back in the response. When the request has no `id`, or
|
||||
when the request body cannot be parsed as JSON, the response `id` is `null`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Methods
|
||||
|
||||
There are 12 methods. Capabilities advertised by `automation.version` list the 11
|
||||
callable feature methods (every method except `automation.version` itself).
|
||||
|
||||
### `automation.version`
|
||||
|
||||
Returns server identity and the list of supported methods. Takes no parameters.
|
||||
|
||||
**Result:**
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"protocol": "2.0",
|
||||
"capabilities": [
|
||||
"tree.dump", "tree.find", "widget.get", "input.click", "input.type",
|
||||
"input.key", "sync.wait_for", "app.state", "screenshot.window", "file.open",
|
||||
"view.select"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `tree.dump`
|
||||
|
||||
Snapshot the live UI tree as a single root node with nested children.
|
||||
|
||||
**Params (all optional):**
|
||||
|
||||
| Param | Type | Default | Meaning |
|
||||
|---|---|---|---|
|
||||
| `root` | string (id or path) | full tree | Root the dump at the node with this id/path. |
|
||||
| `max_depth` | int | `-1` | Maximum depth to descend. `-1` = unlimited. |
|
||||
| `visible_only` | bool | `false` | When true, omit non-visible nodes. |
|
||||
| `include_imgui` | bool | `true` | When true, include ImGui items. |
|
||||
|
||||
**Result:** the serialized root [node](#5-unified-node-shape), with `children`
|
||||
included.
|
||||
|
||||
### `tree.find`
|
||||
|
||||
Find all nodes matching a [target predicate](#6-target--locator).
|
||||
|
||||
**Params:** a target predicate — any combination of `name`, `class`, `label`,
|
||||
`value`, `backend` (provided fields are ANDed). The params object is the target
|
||||
itself (it is *not* wrapped in a `target` key for this method).
|
||||
|
||||
**Result:** a **flat JSON array** of matching nodes. The nodes in this array are
|
||||
returned **without** their `children` (use `widget.get`/`tree.dump` to descend).
|
||||
|
||||
### `widget.get`
|
||||
|
||||
Fetch a single node by [target](#6-target--locator).
|
||||
|
||||
**Params:**
|
||||
|
||||
| Param | Type | Required | Meaning |
|
||||
|---|---|---|---|
|
||||
| `target` | object | yes | Target spec (id / path / predicate). |
|
||||
|
||||
**Result:** a single [node](#5-unified-node-shape), with its `children` included.
|
||||
|
||||
**Errors:** `1001` if the target is **not found** *or* **ambiguous** (more than one
|
||||
match).
|
||||
|
||||
### `input.click`
|
||||
|
||||
Click a resolved, actionable node.
|
||||
|
||||
**Params:**
|
||||
|
||||
| Param | Type | Default | Meaning |
|
||||
|---|---|---|---|
|
||||
| `target` | object | required | Target spec; must resolve to exactly one node. |
|
||||
| `button` | string | `"left"` | `"left"`, `"right"`, or `"middle"`. |
|
||||
| `double` | bool | `false` | Double-click when true. |
|
||||
| `modifiers` | array of string | `[]` | Held modifiers: any of `"ctrl"`, `"shift"`, `"alt"`, `"cmd"` (`"meta"` is accepted as an alias of `"cmd"`). |
|
||||
|
||||
**Result:** `{ "ok": true }`.
|
||||
|
||||
**Errors:** `1001` not found / ambiguous; `1002` if the target is disabled or
|
||||
hidden (not actionable). The click path raises and focuses the target's top-level
|
||||
window before injecting the click.
|
||||
|
||||
### `input.type`
|
||||
|
||||
Type text into the currently focused control.
|
||||
|
||||
**Params:**
|
||||
|
||||
| Param | Type | Required | Meaning |
|
||||
|---|---|---|---|
|
||||
| `text` | string | yes | The text to type. |
|
||||
| `target` | object | no | If given, this node is clicked first (to focus it) before typing. |
|
||||
|
||||
**Result:** `{ "ok": true }`.
|
||||
|
||||
**Errors:** if `target` is supplied, the same actionability errors as
|
||||
`input.click` apply (`1001` / `1002`).
|
||||
|
||||
### `input.key`
|
||||
|
||||
Send a key chord (a key plus optional modifiers) to the focused window.
|
||||
|
||||
**Params:**
|
||||
|
||||
| Param | Type | Required | Meaning |
|
||||
|---|---|---|---|
|
||||
| `keys` | string or array | yes | Either a `"+"`-joined string like `"ctrl+s"`, or an array like `["ctrl", "s"]`. The last token is the key; earlier tokens are modifiers. |
|
||||
|
||||
**Result:** `{ "ok": true }`.
|
||||
|
||||
**Key names must be lowercase.** Recognized key names include `"enter"`, `"tab"`,
|
||||
`"esc"`, `"space"`, `"delete"`, `"backspace"`, `"f5"` (and other function keys),
|
||||
and single characters (e.g. `"s"`, `"a"`). Recognized modifiers are `"ctrl"`,
|
||||
`"shift"`, `"alt"`, `"cmd"` (with `"meta"` as an alias for `"cmd"`).
|
||||
**Unrecognized or uppercase key names are silently ignored** — no error is
|
||||
returned, the key simply does not fire. Use lowercase names exclusively.
|
||||
|
||||
### `sync.wait_for`
|
||||
|
||||
Poll the UI until a target node reaches a desired state, or time out. This is the
|
||||
preferred way to synchronize with asynchronous UI changes (it replaces fragile
|
||||
fixed sleeps). Internally it repeatedly refreshes and dumps the tree, re-resolves
|
||||
the target, and evaluates the requested state until it is satisfied.
|
||||
|
||||
**Params:**
|
||||
|
||||
| Param | Type | Default | Meaning |
|
||||
|---|---|---|---|
|
||||
| `target` | object | required | Target spec. |
|
||||
| `state` | string | required | One of `"exists"`, `"visible"`, `"enabled"`, `"value"`. |
|
||||
| `value` | string | — | Required when `state` is `"value"`; the expected value to match. |
|
||||
| `timeout_ms` | int | `5000` | Maximum time to wait, in milliseconds. |
|
||||
| `poll_ms` | int | `100` | Poll interval, in milliseconds (minimum 1). |
|
||||
|
||||
State semantics:
|
||||
|
||||
- `exists` — the target resolves to a node.
|
||||
- `visible` — the node exists and is visible.
|
||||
- `enabled` — the node exists and is **both enabled and visible**.
|
||||
- `value` — the node has a value and that value equals the supplied `value`.
|
||||
|
||||
**Result:** `{ "ok": true, "elapsed_ms": <int> }`.
|
||||
|
||||
**Errors:** `1003` on timeout (the state was not reached within `timeout_ms`).
|
||||
|
||||
### `app.state`
|
||||
|
||||
Return a high-level application-state snapshot. Takes no parameters.
|
||||
|
||||
**Result:**
|
||||
|
||||
```json
|
||||
{
|
||||
"active_tab": "<string>",
|
||||
"project_loaded": <bool>,
|
||||
"slicing": <bool>,
|
||||
"slice_progress": <int>,
|
||||
"foreground": <bool>,
|
||||
"modal_dialog": "<string>"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `active_tab` | The active top-level tab/page. |
|
||||
| `project_loaded` | Whether a project/model is currently loaded. |
|
||||
| `slicing` | Whether slicing is currently in progress. |
|
||||
| `slice_progress` | Slicing progress (`-1` when unknown). |
|
||||
| `foreground` | Whether the main window is in the foreground. |
|
||||
| `modal_dialog` | Present only when a modal dialog is active; identifies it. Omitted otherwise. |
|
||||
|
||||
### `screenshot.window`
|
||||
|
||||
Capture a window as a PNG, exactly as it appears on screen.
|
||||
|
||||
**Params:**
|
||||
|
||||
| Param | Type | Default | Meaning |
|
||||
|---|---|---|---|
|
||||
| `target` | object | main frame | If given, capture this window; otherwise capture the main frame. |
|
||||
|
||||
**Result:** `{ "png_base64": "<base64 PNG>", "width": <int>, "height": <int> }`.
|
||||
|
||||
**Errors:** `1005` on screenshot failure; `1001` if a supplied `target` is not
|
||||
found or ambiguous.
|
||||
|
||||
**How it works:** the window's on-screen rectangle is read back from the
|
||||
DWM-composited desktop framebuffer (`wxScreenDC`), so the capture includes every
|
||||
native child control, the OpenGL 3D viewport, and ImGui overlays — it is a faithful
|
||||
image of what the user sees. (Capturing the parent window's own client DC instead
|
||||
would clip out child HWNDs and the GL surface, leaving them black; that is why this
|
||||
method reads from the screen.)
|
||||
|
||||
**Caveats:**
|
||||
|
||||
- The window must be **visible and unobscured**. Because the source is the on-screen
|
||||
framebuffer, any overlapping window occludes the captured region. The backend
|
||||
raises the target window before capturing.
|
||||
- **HiDPI:** the reported `width`/`height` come from the window's logical client size,
|
||||
while the screen framebuffer is in physical pixels. On per-monitor-DPI displays the
|
||||
two can differ; the capture may be cropped or scaled relative to the logical size.
|
||||
- Because the capture is the live on-screen image, the 3D content reflects the
|
||||
**current view**: the model in the 3D editor, or the gcode toolpaths in Preview
|
||||
after a slice. There is no separate offscreen 3D-render method — the window
|
||||
capture already includes whatever the GL canvas is showing.
|
||||
|
||||
### `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.
|
||||
|
||||
### `view.select`
|
||||
|
||||
Switch the main window to a top-level view/tab at runtime. Useful to put the UI in a
|
||||
known state before other actions — e.g. switch to **Prepare** before loading a model,
|
||||
or to **Preview** after slicing.
|
||||
|
||||
**Params:**
|
||||
|
||||
| Param | Type | Required | Meaning |
|
||||
|---|---|---|---|
|
||||
| `view` | string | yes | The target view. One of: `home`, `prepare` (3D editor), `preview` (sliced G-code), `device`, `multi_device`, `project`, `calibration`. |
|
||||
|
||||
**Result:** `{ "ok": true, "view": <string>, "index": <int> }`, where `index` is the
|
||||
resulting tab index (it can vary with layout, since some tabs — e.g. `multi_device` —
|
||||
are only present in certain configurations).
|
||||
|
||||
**Errors:**
|
||||
|
||||
- `-32602` (invalid params) — `view` is missing, is not a string, or is empty.
|
||||
- `1001` (not found) — the view name is unknown, or that view is not available in the
|
||||
current layout (for example `prepare`/`preview` in G-code-viewer mode, or
|
||||
`multi_device` when multi-device is disabled).
|
||||
- `1004` (GUI busy) — the GUI-thread marshal timed out.
|
||||
|
||||
---
|
||||
|
||||
## 5. Unified node shape
|
||||
|
||||
Both wx widgets and ImGui items are reported with the same node schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"backend": "wx" | "imgui",
|
||||
"id": "<string>",
|
||||
"path": "<string>",
|
||||
"class": "<string>",
|
||||
"label": "<string>",
|
||||
"rect": { "x": <int>, "y": <int>, "w": <int>, "h": <int> },
|
||||
"enabled": <bool>,
|
||||
"visible": <bool>,
|
||||
"value": "<string>",
|
||||
"children": [ <node>, ... ]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `backend` | `"wx"` for native wxWidgets controls, `"imgui"` for immediate-mode ImGui items. |
|
||||
| `id` | The automation id when one is set, otherwise a derived id. For ImGui items the `path` doubles as the `id`. |
|
||||
| `path` | Positional path, e.g. `"MainFrame/Panel[2]/Button[0]"`. For ImGui items: `"ImGui/<window>/<label>"`. |
|
||||
| `class` | wx class name, or the ImGui item type. |
|
||||
| `label` | The control's label/caption. May include an ImGui `##`-id suffix for ImGui items. |
|
||||
| `rect` | Bounding rectangle in **screen coordinates**. |
|
||||
| `enabled` | Whether the control is enabled. |
|
||||
| `visible` | Whether the control is visible. |
|
||||
| `value` | The control's value (text/choice/check/slider, etc.). **Omitted entirely** when the control has no applicable value. |
|
||||
| `children` | Child nodes. **wx only**, and present only when children are included (e.g. `tree.dump`, `widget.get`). ImGui items are flat (no children) and are listed under their window. |
|
||||
|
||||
Notes:
|
||||
|
||||
- The `value` key is **omitted** (not `null`) when the control has no value.
|
||||
- `children` is present only for wx nodes when children are requested; ImGui nodes
|
||||
never carry `children`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Target / locator
|
||||
|
||||
Most methods accept a **target** object that identifies one or more nodes. A
|
||||
target may specify:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `id` | Exact automation id. |
|
||||
| `path` | Exact positional path. |
|
||||
| `name` | Predicate: matches either the node's `id` **or** its `label`. |
|
||||
| `class` | Predicate: exact class name. |
|
||||
| `label` | Predicate: exact label. |
|
||||
| `value` | Predicate: node has a value and it equals this string. |
|
||||
| `backend` | Predicate: `"wx"` or `"imgui"`. |
|
||||
|
||||
**Resolution order:** **`id` → `path` → predicate.**
|
||||
|
||||
- If `id` is present, only `id` is used (exact match).
|
||||
- Else if `path` is present, only `path` is used (exact match).
|
||||
- Else the predicate fields (`name`, `class`, `label`, `value`, `backend`) are
|
||||
used, and all provided predicate fields are **ANDed** together.
|
||||
|
||||
Action methods (`input.click`, `input.type` with a target, `widget.get`, and
|
||||
single-target `screenshot.window`) require a **unique** match. If the target
|
||||
resolves to zero matches or more than one match, the call fails with error `1001`
|
||||
(not found / ambiguous). `tree.find` is the exception: it returns *all* matches as
|
||||
an array and never errors on ambiguity.
|
||||
|
||||
---
|
||||
|
||||
## 7. Error codes
|
||||
|
||||
Standard JSON-RPC codes:
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `-32700` | Parse error — the request body was not valid JSON. |
|
||||
| `-32600` | Invalid request — missing/invalid `method`. |
|
||||
| `-32601` | Method not found — unknown method name. |
|
||||
| `-32602` | Invalid params — missing/invalid parameters for the method. |
|
||||
|
||||
Application-specific codes:
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `1001` | Widget/target not found **or** ambiguous (more than one match). |
|
||||
| `1002` | Not actionable — the target is disabled or hidden. |
|
||||
| `1003` | Wait timeout — `sync.wait_for` did not reach the requested state in time. |
|
||||
| `1004` | GUI thread busy / timeout — a backend call could not be marshaled onto the GUI thread in time (wedged GUI). |
|
||||
| `1005` | Screenshot failed. |
|
||||
| `1006` | Disabled. |
|
||||
| `1007` | Load failed — `file.open`'s `load_files` returned empty or threw (not found, parse error, unsupported format). |
|
||||
|
||||
---
|
||||
|
||||
## 8. Automation-id naming conventions & instrumented ids
|
||||
|
||||
Stable automation ids follow these prefix conventions:
|
||||
|
||||
| Prefix | Used for |
|
||||
|---|---|
|
||||
| `btn_` | Buttons |
|
||||
| `combo_` | Preset combo boxes |
|
||||
| `tab_` | Tabs |
|
||||
| `canvas_` | Canvases |
|
||||
| `dlg_` | Dialog buttons |
|
||||
|
||||
### Instrumented ids (as-built in v1)
|
||||
|
||||
The following controls currently carry stable automation ids:
|
||||
|
||||
| id | Control | Note |
|
||||
|---|---|---|
|
||||
| `btn_slice` | Slice-plate button | |
|
||||
| `btn_export` | Print / Export button | Multi-purpose: the action (Print plate / Export G-code / Send) depends on the current mode. |
|
||||
| `tab_device` | Device / Monitor tab (`MonitorPanel`) | |
|
||||
| `combo_printer` | Printer preset combo (sidebar) | |
|
||||
| `combo_filament` | Filament preset combo (sidebar) | First filament row only; extra multi-material rows are not instrumented. |
|
||||
| `canvas_3d` | 3D editor GL canvas | |
|
||||
|
||||
### Controls NOT instrumented in v1
|
||||
|
||||
Several controls are intentionally **not** instrumented in v1 because they have no
|
||||
stable `wxWindow` target to attach an id to:
|
||||
|
||||
- **`combo_process`** — process settings are not a sidebar combo box in the current
|
||||
OrcaSlicer layout, so there is no combo control to instrument.
|
||||
- **`btn_add`** — the add/import-object control is a `GLToolbar` item rendered
|
||||
*inside* the GL canvas, not a `wxWindow`.
|
||||
- **`tab_prepare` / `tab_preview`** — the Prepare and Preview notebook pages are
|
||||
both backed by the **same** window, and the per-tab buttons are private; there is
|
||||
no distinct stable window to target.
|
||||
|
||||
For controls that are not instrumented, scripts should fall back to class / label /
|
||||
path lookup (for wx controls) or ImGui-item lookup (for ImGui controls).
|
||||
|
||||
---
|
||||
|
||||
## 9. ImGui notes
|
||||
|
||||
ImGui is **immediate-mode**: an item is addressable only while it is being drawn in
|
||||
the current frame. The automation backend records ImGui items each frame, and a
|
||||
`refresh_ui` is forced before every read or action so that the latest frame's items
|
||||
are captured.
|
||||
|
||||
Consequences and conventions:
|
||||
|
||||
- Use [`sync.wait_for`](#syncwait_for) to wait for a transient gizmo or panel item
|
||||
to appear before acting on it.
|
||||
- ImGui items are reported with `backend: "imgui"`, a `path` of the form
|
||||
`ImGui/<window>/<label>`, and that **path doubles as the item's `id`** in v1.
|
||||
- ImGui items are **flat** — they have no `children` and are listed under their
|
||||
window.
|
||||
- Labels may include ImGui `##`-id suffixes (the part after `##` that ImGui uses to
|
||||
disambiguate identically labeled widgets).
|
||||
- Raw `ImGui::` gizmos that are *not* routed through the instrumented
|
||||
`ImGuiWrapper` widgets (for example some Emboss / SVG / Text gizmo controls) are
|
||||
only covered at the **window level** in v1; their individual sub-items are not
|
||||
enumerated.
|
||||
|
||||
---
|
||||
|
||||
## 10. Platform & display caveats
|
||||
|
||||
- **Input requires a focused, visible window.** OS-level input injection uses
|
||||
`wxUIActionSimulator`, which requires a focused, visible window. The click path
|
||||
raises and focuses the target's top-level window first.
|
||||
- **Linux CI needs a display.** There must be an X display available; wrap test
|
||||
runs with `xvfb-run` (for example, `xvfb-run -a python example_slice.py ...`).
|
||||
- **Input is asynchronous.** Do **not** rely on fixed sleeps. Use
|
||||
[`sync.wait_for`](#syncwait_for) — for example, wait for `btn_export` to become
|
||||
`enabled` after slicing completes — rather than sleeping for a guessed duration.
|
||||
- **`screenshot.window` reads the screen.** It captures the on-screen, DWM-composited
|
||||
framebuffer, so the target window must be visible and unobscured, and the result is
|
||||
in physical pixels (see HiDPI caveat under [`screenshot.window`](#screenshotwindow)).
|
||||
The capture includes the GL 3D viewport as currently shown (model or toolpaths).
|
||||
- **Single-client / serialized.** v1 handles one request at a time; issue requests
|
||||
sequentially from a single client.
|
||||
- **GUI-thread marshaling.** Every backend call is marshaled onto the GUI thread
|
||||
with a timeout. A wedged or unresponsive GUI returns error `1004`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Quick start
|
||||
|
||||
Using the reference client in `tools/automation/orca_automation.py`:
|
||||
|
||||
```python
|
||||
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)
|
||||
```
|
||||
|
||||
For a full, runnable end-to-end example — launching OrcaSlicer with the automation
|
||||
flags, loading a model, slicing, waiting for completion, and saving a window PNG —
|
||||
see `tools/automation/example_slice.py`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Future work
|
||||
|
||||
Planned enhancements beyond v1:
|
||||
|
||||
- **Authentication token** plus a Preferences toggle to enable/disable the server
|
||||
from the GUI.
|
||||
- **WebSocket push events** for real-time UI/state notifications (instead of
|
||||
polling).
|
||||
- **Per-item ImGui gizmo instrumentation** so individual gizmo sub-controls (Emboss
|
||||
/ SVG / Text, etc.) are addressable, not just at the window level.
|
||||
- **More widget ids** — the process combo, the add/import button, and the
|
||||
Prepare/Preview tabs once they expose stable windows.
|
||||
- An **MCP wrapper** to expose the automation surface to model-context tooling.
|
||||
|
||||
---
|
||||
|
||||
## Verification (v1)
|
||||
|
||||
This section records the final regression gate for the v1 feature: confirmation
|
||||
that the protocol core is covered by unit tests, that the existing test suites
|
||||
are unaffected, and that the **disabled path (automation OFF, the default) is a
|
||||
true no-op** — zero new threads, zero socket binds, zero allocations, and zero
|
||||
behavior change.
|
||||
|
||||
### Unit-suite results (Release, Windows / MSVC, Ninja Multi-Config)
|
||||
|
||||
| Suite | Result |
|
||||
|---|---|
|
||||
| `automation` (protocol core) | **32 / 32 passed** |
|
||||
| `libslic3r` (most affected by the additive `PrintConfig.cpp` CLI options) | **99 / 99 passed** |
|
||||
| `fff_print` | **14 / 14 passed** |
|
||||
| `libnest2d` | **14 / 14 passed** |
|
||||
| `sla_print` | **21 / 21 passed** |
|
||||
| `slic3rutils` | 3 / 5 passed — 2 pre-existing `[OrcaCloudServiceAgent]` SEGFAULTs, **unrelated to automation** (see note) |
|
||||
|
||||
> The two `slic3rutils` failures are `Orca cloud flat/nested session resolves
|
||||
> display name consistently`. They exercise `Slic3r::OrcaCloudServiceAgent`, which
|
||||
> the automation branch does **not** touch (verified via `git diff --stat
|
||||
> main...HEAD` — no change to `src/slic3r/Utils/OrcaCloudServiceAgent.*` or
|
||||
> `tests/slic3rutils/*`). They are pre-existing and not a regression introduced by
|
||||
> this feature.
|
||||
|
||||
### Static disabled-path audit (the core regression guarantee)
|
||||
|
||||
Verified by code reading that with no `--automation-server` flag:
|
||||
|
||||
- **Flag defaults off.** `m_automation_port` defaults to `0`
|
||||
(`src/slic3r/GUI/GUI_App.hpp:249`); `is_automation_enabled()` returns
|
||||
`m_automation_port > 0` (`GUI_App.hpp:386`) → `false` by default.
|
||||
- **No server / thread / socket.** `post_init()` calls
|
||||
`start_automation_server()` **only** when
|
||||
`init_params->automation_port > 0` (`src/slic3r/GUI/GUI_App.cpp:737-740`), and
|
||||
`start_automation_server()` itself early-returns when `m_automation_port <= 0`
|
||||
(`GUI_App.cpp:7097`). The backend / dispatcher / beast server objects are
|
||||
constructed nowhere else → no `orca_automation` thread and no localhost bind
|
||||
when the flag is absent.
|
||||
- **Recording hooks short-circuit.** `ImGuiWrapper::automation_record_last_item`
|
||||
has as its **first statement** `if (!wxGetApp().is_automation_enabled())
|
||||
return;` (`src/slic3r/GUI/ImGuiWrapper.cpp:576-577`) — a single bool check, no
|
||||
`ImGuiItemRecord` allocation and no `ImGuiItemTable` access on the disabled
|
||||
path. In `ImGuiWrapper::render()` the window-enumeration loop and
|
||||
`swap_frame()` are fully wrapped in `if (wxGetApp().is_automation_enabled())`
|
||||
(`ImGuiWrapper.cpp:599-611`); when off, `render()` is its original
|
||||
`ImGui::Render()` + `render_draw_data()` plus one bool check.
|
||||
- **Instrumentation is inert.** The ~7 `set_automation_id(...)` calls
|
||||
(`MainFrame.cpp:1330,1389,1841,1842`; `Plater.cpp:1772,2172,5068`) only store a
|
||||
pointer into a static registry and bind a `wxEVT_DESTROY` pruning handler
|
||||
(`src/slic3r/GUI/Automation/AutomationRegistry.cpp:24-36`). The registry is
|
||||
**read** only via `window_for_automation_id` / `automation_id_of`, which are
|
||||
called solely by the backend while the server is running → harmless when off.
|
||||
- **CLI options are purely additive.** `automation_server` (coBool, default
|
||||
`false`) and `automation_server_port` (coInt, default `13619`) are new `add()`
|
||||
entries appended after `enable_timelapse`
|
||||
(`src/libslic3r/PrintConfig.cpp:10794-10805`); no existing option is changed.
|
||||
`GUI_InitParams::automation_port` defaults to `0`
|
||||
(`src/slic3r/GUI/GUI_Init.hpp:37`) and is set only when `--automation-server`
|
||||
is supplied (`src/OrcaSlicer.cpp:1345-1348`).
|
||||
|
||||
**Conclusion:** with automation OFF (the default), the feature allocates nothing
|
||||
and changes nothing — the only added cost on any hot path is a single boolean
|
||||
comparison.
|
||||
|
||||
### Deferred manual runtime checks (require a display / Xvfb)
|
||||
|
||||
These need a live GUI and cannot be run headlessly in CI; they are the manual
|
||||
acceptance steps:
|
||||
|
||||
1. Launch **without** `--automation-server` → `curl http://127.0.0.1:13619/`
|
||||
fails to connect (no listener); no `orca_automation` thread exists.
|
||||
2. Launch **with** `--automation-server --automation-server-port=13619` →
|
||||
`GET /` returns the health text; `POST /jsonrpc {"method":"automation.version"}`
|
||||
returns version / protocol / capabilities; `widget.get {"target":{"id":"btn_slice"}}`
|
||||
returns a node with a sensible screen rect.
|
||||
3. Interactive sanity: open a gizmo / move sliders with automation OFF → no
|
||||
visual or behavior change.
|
||||
|
||||
See `tools/automation/example_slice.py` for the runnable end-to-end path.
|
||||
@@ -1,608 +0,0 @@
|
||||
# `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.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,117 +0,0 @@
|
||||
# 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> }`
|
||||
- `count` is `load_files(...).size()` — the number of objects added to the scene.
|
||||
- **Errors:**
|
||||
| Code | Constant | Condition |
|
||||
|------|----------|-----------|
|
||||
| -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** |
|
||||
|
||||
## 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<std::string>& 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` → `-32602` (`kInvalidParams`)
|
||||
- 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.
|
||||
@@ -1,314 +0,0 @@
|
||||
# OrcaSlicer UI Automation — Design Spec
|
||||
|
||||
**Date:** 2026-06-03
|
||||
**Status:** Approved design, pending implementation plan
|
||||
**Topic:** Add an opt-in, externally-controllable UI automation interface to OrcaSlicer for automated GUI testing and future AI-agent control.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Add a localhost JSON-RPC server to a running OrcaSlicer GUI instance that lets an
|
||||
**external** script (or AI agent) drive and observe the real GUI. "Driving" is done
|
||||
the way a user would — simulated mouse/keyboard via `wxUIActionSimulator` — while
|
||||
"observing" reads the live widget state and captures screenshots.
|
||||
|
||||
The interface must cover **three UI technologies** present in OrcaSlicer:
|
||||
|
||||
1. Native **wxWidgets** widgets (`wxWindow` hierarchy).
|
||||
2. The **OpenGL 3D viewport** (`GLCanvas3D`) — screenshots via the existing
|
||||
framebuffer/thumbnail path.
|
||||
3. **Dear ImGui** immediate-mode controls (gizmo panels, in-canvas overlays,
|
||||
notifications) — recorded as they are drawn, because there is no persistent tree.
|
||||
|
||||
## 2. Goals / Non-Goals
|
||||
|
||||
### Goals
|
||||
- Let an external, language-agnostic client connect to a running instance and:
|
||||
introspect the UI, locate widgets by stable name, perform input actions, wait on
|
||||
conditions, and capture screenshots (including the 3D view as a separate image).
|
||||
- Be safe by default: disabled unless explicitly enabled; bound to `127.0.0.1` only.
|
||||
- Be testable in CI without a display (pure-logic units behind a mock backend).
|
||||
- Ship a reference Python client, a runnable end-to-end example, protocol docs, and
|
||||
C++ unit tests.
|
||||
|
||||
### Non-Goals (v1)
|
||||
- No headless/offscreen automation — OS input injection needs a focused, visible
|
||||
window (Linux CI requires a display, e.g. Xvfb).
|
||||
- No auth token in v1 (documented future hardening; localhost-only is the boundary).
|
||||
- No per-item coverage of raw-`ImGui::` gizmos (Emboss/SVG/Text). They get
|
||||
window-level coverage; per-item is future work.
|
||||
- No new scripting language embedded in the app; control is purely external over JSON-RPC.
|
||||
- We do **not** modify the existing auth `HttpServer`.
|
||||
|
||||
## 3. Background — existing infrastructure (verified)
|
||||
|
||||
- **`wxUIActionSimulator`** is compiled into the wx build and already used
|
||||
(`src/slic3r/GUI/GUI_ObjectList.cpp:211`). Cross-platform simulated input is available.
|
||||
- **`GLCanvas3D::render_thumbnail()`** (`src/slic3r/GUI/GLCanvas3D.cpp:2210+`) →
|
||||
`render_thumbnail_framebuffer()` (`:6352`) renders the 3D scene into a
|
||||
`ThumbnailData` (RGBA) via an FBO + `glReadPixels`. `debug_output_thumbnail()`
|
||||
(`:6099`) shows the `ThumbnailData → wxImage → PNG` conversion. This is the
|
||||
separate-3D-screenshot path. `Plater::generate_thumbnail()`/`generate_thumbnails()`
|
||||
wrap it.
|
||||
- **`ImGuiWrapper`** (`src/slic3r/GUI/ImGuiWrapper.hpp`) is the chokepoint for nearly
|
||||
all ImGui controls: `button`/`bbl_button`, `checkbox`/`bbl_checkbox`, `combo`,
|
||||
`slider_float`, `input_double`, `radio_button`, `menu_item_with_icon`, plus
|
||||
`begin`/`end` for windows. `imgui_internal.h` is in-tree, exposing
|
||||
`ImGui::GetCurrentContext()->Windows`, item rects, and hovered/active id.
|
||||
- **`HttpServer`** (`src/slic3r/GUI/HttpServer.{hpp,cpp}`, boost::beast, port 13618)
|
||||
is used for cloud auth. **It cannot serve a POST body** — `session::read_body()`
|
||||
reads and discards the body and never replies (`HttpServer.cpp:57-65`). It is
|
||||
effectively GET-only. We will **not** reuse or modify it.
|
||||
- **`OtherInstanceMessageHandler`** (`src/slic3r/GUI/InstanceCheck.{hpp,cpp}`) is the
|
||||
template for "start a localhost listener once the MainFrame exists, post events into
|
||||
the GUI." Useful as a structural reference.
|
||||
- **CLI args** are parsed in `src/OrcaSlicer.cpp` (`CLI::setup`/`CLI::run`) and flow
|
||||
into the GUI run params consumed by `GUI_App::OnInit()`.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
```
|
||||
External script / AI agent ──► Python client (orca_automation.py)
|
||||
│ HTTP POST /jsonrpc (JSON-RPC 2.0) on 127.0.0.1:<port>
|
||||
▼
|
||||
AutomationServer (dedicated boost::beast listener; own thread;
|
||||
│ started only when --automation-server is set)
|
||||
│ parse JSON-RPC envelope
|
||||
▼
|
||||
JsonRpcDispatcher (pure logic — method registry; unit-testable)
|
||||
│ marshal each call to the GUI thread via wxGetApp().CallAfter
|
||||
│ + std::promise/future with a per-request timeout
|
||||
▼
|
||||
IUiBackend (interface) ──► WxUiBackend (real, GUI thread) / MockBackend (tests)
|
||||
├─ Introspection : walk wxWindow tree + read ImGui item table → unified JSON
|
||||
├─ Locator : resolve automation-id / predicate → wx widget or ImGui item
|
||||
├─ Actions : raise window, then wxUIActionSimulator click/type/key
|
||||
├─ Sync : wait_for (poll condition) + app.state snapshot
|
||||
└─ Screenshots : wx widget → wxDC→PNG ; 3D view → render_thumbnail()→PNG
|
||||
```
|
||||
|
||||
### Components (new files unless noted)
|
||||
|
||||
All new code lives under `src/slic3r/GUI/Automation/`.
|
||||
|
||||
| Component | Responsibility |
|
||||
|---|---|
|
||||
| `AutomationServer.{hpp,cpp}` | Dedicated boost::beast HTTP listener with **POST + body** support; one `POST /jsonrpc` endpoint; returns `application/json`. Localhost-only. Own thread. |
|
||||
| `JsonRpcDispatcher.{hpp,cpp}` | Parse JSON-RPC 2.0; route `method` → handler; build result/error. Depends only on `IUiBackend`. No wx/ImGui includes → unit-testable. |
|
||||
| `IUiBackend.hpp` | Abstract interface: `dump_tree`, `find`, `get_widget`, `click`, `type`, `key`, `wait_for`, `app_state`, `screenshot_window`, `screenshot_viewport3d`. Uses plain structs (no wx types) so tests can mock it. |
|
||||
| `WxUiBackend.{hpp,cpp}` | Real implementation. Runs on GUI thread. Walks `wxWindow` tree, reads the ImGui item table, drives `wxUIActionSimulator`, captures screenshots. |
|
||||
| `MockUiBackend.{hpp,cpp}` (tests) | Deterministic fake tree + recorded actions for unit tests. |
|
||||
| `AutomationRegistry.{hpp,cpp}` | Process-wide `wxWindow* → automation_id` map + reverse lookup; `set_automation_id(win, "id")` helper. Header is dependency-light so widget-construction code can call the helper unconditionally (it is a cheap no-op-safe registration). |
|
||||
| `WidgetSerializer.{hpp,cpp}` | `wxWindow` → JSON node (name/id, class, label, screen-rect, enabled, shown, value via RTTI). |
|
||||
| `ImGuiItemTable.{hpp,cpp}` | Per-frame recorder of ImGui items + live-window enumeration. Populated from `ImGuiWrapper`; read on GUI thread. |
|
||||
|
||||
### Touch points in existing files
|
||||
- `src/slic3r/GUI/ImGuiWrapper.cpp` — add recording hooks inside the wrapped widget
|
||||
methods and `begin`/`end`, **guarded by an `is_automation_enabled()` flag** so there
|
||||
is zero overhead and zero behavior change when automation is off.
|
||||
- `src/slic3r/GUI/GUI_App.{hpp,cpp}` — own the `AutomationServer`; start it in
|
||||
`OnInit()` only when the flag is set; stop it on exit. Expose
|
||||
`is_automation_enabled()`.
|
||||
- `src/OrcaSlicer.cpp` — parse `--automation-server[=PORT]`; pass through GUI run params.
|
||||
- A handful of widget-construction sites (Slice/Export buttons, preset combos, main
|
||||
tabs, common dialog OK/Cancel, the 3D canvas) — add `set_automation_id(...)` calls
|
||||
(~15-20 widgets in v1).
|
||||
- CMake: add the new `Automation/` sources to the GUI target; add the unit-test target.
|
||||
|
||||
## 5. Transport & Protocol
|
||||
|
||||
- **Transport:** HTTP/1.1 on `127.0.0.1:<port>` (default **13619**, adjacent to the
|
||||
auth server's 13618). Single endpoint: `POST /jsonrpc`, body is a JSON-RPC 2.0
|
||||
request, response is a JSON-RPC 2.0 result/error. `GET /` returns a tiny health/version page.
|
||||
- **Protocol:** JSON-RPC 2.0. `id`, `method`, `params`. Batch not required in v1.
|
||||
|
||||
### v1 methods
|
||||
|
||||
| Method | Params | Result |
|
||||
|---|---|---|
|
||||
| `automation.version` | — | `{version, protocol, capabilities[]}` |
|
||||
| `tree.dump` | `{root?, max_depth?, visible_only?, include_imgui?}` | tree of nodes (wx + imgui) |
|
||||
| `tree.find` | `{name?, class?, label?, value?, backend?}` | `[node...]` matches |
|
||||
| `widget.get` | `{target}` | single node detail |
|
||||
| `input.click` | `{target, button?=left, double?=false, modifiers?[]}` | `{ok}` |
|
||||
| `input.type` | `{target?, text}` | `{ok}` |
|
||||
| `input.key` | `{keys}` e.g. `"ctrl+s"` or `["ctrl","s"]` | `{ok}` |
|
||||
| `sync.wait_for` | `{target, state: exists\|visible\|enabled\|value, value?, timeout_ms?=5000, poll_ms?=100}` | `{ok, elapsed_ms}` |
|
||||
| `app.state` | — | `{active_tab, project_loaded, slicing, slice_progress, modal_dialog?, foreground}` |
|
||||
| `screenshot.window` | `{target?}` (default main frame) | `{png_base64, width, height}` |
|
||||
| `screenshot.viewport3d` | `{plate?, width?, height?}` | `{png_base64, width, height}` |
|
||||
|
||||
### Node shape (unified for wx and ImGui)
|
||||
```json
|
||||
{
|
||||
"backend": "wx" | "imgui",
|
||||
"id": "btn_slice", // automation id if set, else derived path id
|
||||
"path": "MainFrame/.../btn_slice", // stable-ish positional path
|
||||
"class": "Button", // wx class name or imgui item type
|
||||
"label": "Slice plate",
|
||||
"rect": { "x": 100, "y": 200, "w": 120, "h": 32 }, // screen coords
|
||||
"enabled": true,
|
||||
"visible": true,
|
||||
"value": "PLA", // when applicable (text/choice/check/slider)
|
||||
"children": [ ... ] // wx only; imgui items are flat under their window
|
||||
}
|
||||
```
|
||||
|
||||
### Error model (JSON-RPC `error.code`)
|
||||
- `-32700` parse error, `-32601` method not found, `-32602` invalid params (standard).
|
||||
- Application codes: `1001` widget/target not found, `1002` target not actionable
|
||||
(disabled/hidden), `1003` wait timeout, `1004` GUI thread busy/timeout,
|
||||
`1005` screenshot failed, `1006` automation feature disabled.
|
||||
|
||||
## 6. Threading model
|
||||
|
||||
- `AutomationServer` runs on its own thread and accepts connections; the dispatcher
|
||||
parses on that thread.
|
||||
- **Every** call touching wx/ImGui/GL is marshaled to the GUI thread with
|
||||
`wxGetApp().CallAfter([...]{ ... })`; the server thread blocks on a `std::future`
|
||||
with a per-request timeout (default 5 s; `wait_for` uses its own larger budget).
|
||||
Timeout → error `1004`.
|
||||
- This is mandatory: wx widgets, the ImGui context, and the GL context are not
|
||||
thread-safe and are owned by the GUI thread.
|
||||
- `CallAfter` is serviced even while modal dialogs run (nested event loop), so
|
||||
automation can interact with dialogs.
|
||||
|
||||
## 7. Widget locator & automation IDs (wxWidgets)
|
||||
|
||||
- **Stable IDs:** `set_automation_id(window, "btn_slice")` registers the widget in
|
||||
`AutomationRegistry`. Stored in a side map keyed by `wxWindow*` (not via `SetName`,
|
||||
to avoid any coupling with wx's name-based lookups). Registration is removed on
|
||||
widget destruction (bind to `wxEVT_DESTROY` or prune lazily on lookup).
|
||||
- **Derived IDs:** for un-instrumented widgets, `WidgetSerializer` derives a positional
|
||||
`path` (e.g. `MainFrame/Panel[2]/Button[0]`) so an AI agent can still target anything.
|
||||
Named IDs are the preferred, stable path.
|
||||
- **Locator resolution order:** exact automation id → exact path → predicate match
|
||||
(name/class/label/value). Ambiguous matches return the list via `tree.find`; action
|
||||
methods require a unique match or error `1001`.
|
||||
- **v1 instrumented widgets (~15-20):** Slice/Export buttons, printer & filament preset
|
||||
combos, the main tab buttons (`tp3DEditor`/`tpPreview`/`tpMonitor`/…), Add/Import,
|
||||
common dialog OK/Cancel/Yes/No, the `GLCanvas3D` itself.
|
||||
|
||||
## 8. ImGui coverage (v1 = wrapper items + window introspection)
|
||||
|
||||
- **Item recording:** inside each `ImGuiWrapper` wrapped method, when
|
||||
`is_automation_enabled()` is true, append the just-drawn item to a per-frame
|
||||
`ImGuiItemTable` entry: `{window_name, label/id, type, rect, enabled, value}`.
|
||||
Item rect comes from `ImGui::GetItemRectMin/Max()` (ImGui display coords) mapped to
|
||||
**screen** coords via the `GLCanvas3D` client origin (`ClientToScreen`) and DPI scale.
|
||||
- **Window enumeration:** via `imgui_internal.h`, enumerate `GetCurrentContext()->Windows`
|
||||
for window name, rect, visibility, plus the global hovered/active item id.
|
||||
- **Double-buffering:** the table is swapped at frame end (`ImGuiWrapper::render`) so
|
||||
readers see a complete frame. Reads happen on the GUI thread (after marshaling), same
|
||||
thread as rendering, so a simple front/back swap suffices.
|
||||
- **Freshness:** because items exist only while drawn, before an ImGui tree read or
|
||||
action the backend forces a canvas refresh and flushes events so the latest frame is
|
||||
captured. `sync.wait_for` can poll for an ImGui item to appear (e.g. after opening a
|
||||
gizmo).
|
||||
- **Actions:** an ImGui target resolves to its recorded screen rect; `input.click`/
|
||||
`input.type` use `wxUIActionSimulator` on that rect — identical action path to wx,
|
||||
different rect source. Typing into an ImGui input works because simulated keystrokes
|
||||
flow through the existing `ImGuiWrapper::update_key_data` bridge once the field is
|
||||
focused by a click.
|
||||
- **Limitation (documented):** raw-`ImGui::` gizmos (Emboss, SVG, Text) are covered at
|
||||
the **window** level only in v1; per-item instrumentation is future work.
|
||||
|
||||
## 9. Screenshots
|
||||
|
||||
- **`screenshot.window`:** capture a `wxWindow` (default: main frame) via
|
||||
`wxClientDC`/`wxWindowDC` → `wxBitmap` → `wxImage` → PNG → base64. Works for native
|
||||
widgets but **not** for the GL canvas region (returns black there) — hence the
|
||||
separate 3D method.
|
||||
- **`screenshot.viewport3d`:** reuse `GLCanvas3D::render_thumbnail()` (FBO +
|
||||
`glReadPixels`) → `ThumbnailData` → `wxImage` (per `debug_output_thumbnail`) → PNG →
|
||||
base64. Optional `plate`, `width`, `height` params. Runs on the GUI thread with the GL
|
||||
context current.
|
||||
|
||||
## 10. Activation & security
|
||||
|
||||
- **Off by default.** Enabled by CLI flag `--automation-server[=PORT]` (default port
|
||||
13619). (An app-config/Preferences toggle may be added later; v1 is flag-only.)
|
||||
- **Bind `127.0.0.1` only.** No external interface.
|
||||
- **No token in v1** (per decision); documented as a recommended future hardening,
|
||||
along with an optional `--automation-token`.
|
||||
- When disabled: no listener, no thread, and all `ImGuiWrapper` recording hooks are
|
||||
skipped — **zero** runtime overhead and **zero** behavior change. This satisfies the
|
||||
project's "features gated by options must not affect existing behavior when disabled"
|
||||
constraint.
|
||||
|
||||
## 11. Testability
|
||||
|
||||
- `JsonRpcDispatcher` depends only on `IUiBackend` and has **no** wx/ImGui/GL includes.
|
||||
- **C++ unit tests (Catch2), display-free, run in CI:**
|
||||
- JSON-RPC envelope parse/validate/dispatch (good + malformed input, error codes).
|
||||
- Method routing and param validation for every v1 method against `MockUiBackend`.
|
||||
- `WidgetSerializer` node shape (fed a synthetic node model, not real wx widgets).
|
||||
- Locator resolution: exact id, path, predicate, ambiguity, not-found.
|
||||
- The only piece needing a real GUI is `WxUiBackend`; it is exercised by the manual
|
||||
end-to-end example, not by CI unit tests.
|
||||
|
||||
## 12. Deliverables
|
||||
|
||||
- **C++:** the `Automation/` components, `ImGuiWrapper` recording hooks, widget
|
||||
instrumentation, CLI flag plumbing, `GUI_App` lifecycle, CMake wiring.
|
||||
- **`tools/automation/orca_automation.py`:** reference Python client wrapping the
|
||||
JSON-RPC calls (`connect`, `version`, `dump_tree`, `find`, `click`, `type`, `key`,
|
||||
`wait_for`, `app_state`, `screenshot`, `screenshot_3d`).
|
||||
- **`tools/automation/example_slice.py`:** runnable end-to-end flow — launch OrcaSlicer
|
||||
with the flag, load a model, click Slice, `wait_for` completion, save a 3D-preview PNG.
|
||||
Doubles as a manual smoke test.
|
||||
- **`doc/automation.md`:** protocol reference (methods, params, results, error codes),
|
||||
node shape, automation-id naming conventions, ImGui notes, platform/display caveats.
|
||||
- **`tests/`:** Catch2 unit-test target for the dispatch/serialize/locator logic.
|
||||
|
||||
## 13. New / changed file inventory
|
||||
|
||||
**New**
|
||||
- `src/slic3r/GUI/Automation/AutomationServer.{hpp,cpp}`
|
||||
- `src/slic3r/GUI/Automation/JsonRpcDispatcher.{hpp,cpp}`
|
||||
- `src/slic3r/GUI/Automation/IUiBackend.hpp`
|
||||
- `src/slic3r/GUI/Automation/WxUiBackend.{hpp,cpp}`
|
||||
- `src/slic3r/GUI/Automation/AutomationRegistry.{hpp,cpp}`
|
||||
- `src/slic3r/GUI/Automation/WidgetSerializer.{hpp,cpp}`
|
||||
- `src/slic3r/GUI/Automation/ImGuiItemTable.{hpp,cpp}`
|
||||
- `tools/automation/orca_automation.py`
|
||||
- `tools/automation/example_slice.py`
|
||||
- `doc/automation.md`
|
||||
- `tests/automation/` (Catch2 target) + `MockUiBackend.{hpp,cpp}`
|
||||
|
||||
**Changed**
|
||||
- `src/slic3r/GUI/ImGuiWrapper.cpp` (guarded recording hooks)
|
||||
- `src/slic3r/GUI/GUI_App.{hpp,cpp}` (server lifecycle, `is_automation_enabled()`)
|
||||
- `src/OrcaSlicer.cpp` (CLI flag)
|
||||
- ~15-20 widget-construction sites (`set_automation_id`)
|
||||
- `src/slic3r/GUI/CMakeLists.txt` + `tests/CMakeLists.txt`
|
||||
|
||||
## 14. Known constraints & limitations
|
||||
|
||||
- OS input injection requires the OrcaSlicer window **focused and visible**; the backend
|
||||
raises/focuses the main window before injecting. Linux CI needs a display (Xvfb).
|
||||
- Input is asynchronous at the OS level; correctness relies on `sync.wait_for` rather
|
||||
than fixed sleeps.
|
||||
- ImGui items are only addressable while their host panel is drawn.
|
||||
- Raw-`ImGui::` gizmos: window-level only in v1.
|
||||
- Single-client assumption in v1 (serialized request handling); no concurrent sessions
|
||||
contract.
|
||||
|
||||
## 15. Future work (out of scope for v1)
|
||||
|
||||
- Optional auth token + Preferences toggle.
|
||||
- WebSocket channel for server-push events (slice progress, dialog-appeared).
|
||||
- Per-item instrumentation for raw-`ImGui::` gizmos.
|
||||
- An MCP server wrapping the JSON-RPC client for direct AI-agent integration.
|
||||
- Optional integration of Dear ImGui Test Engine for deterministic ImGui interaction.
|
||||
|
||||
## 16. Verification plan
|
||||
|
||||
- **CI:** Catch2 unit tests (dispatch/serialize/locator) pass with no display.
|
||||
- **Manual / e2e:** run `tools/automation/example_slice.py` against a built OrcaSlicer
|
||||
launched with `--automation-server`; confirm model loads, Slice runs, `wait_for`
|
||||
returns on completion, and both a wx-window PNG and a 3D-viewport PNG are produced.
|
||||
- **Regression:** build and run with automation **off**; confirm no new threads, no
|
||||
listener, and ImGui rendering is byte-for-byte unchanged (hooks compiled out of the
|
||||
hot path via the disabled flag).
|
||||
@@ -4,7 +4,19 @@
|
||||
# Icezaza, 2026.
|
||||
#
|
||||
msgid ""
|
||||
msgstr "Project-Id-Version: Orca Slicer\nReport-Msgid-Bugs-To: \nPOT-Creation-Date: 2026-05-22 02:24+0800\nPO-Revision-Date: 2026-05-28 22:39+0700\nLast-Translator: Icezaza\nLanguage-Team: Thai\nLanguage: th\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: nplurals=1; plural=0;\nX-Generator: Codex Thai translation bootstrap"
|
||||
msgstr ""
|
||||
"Project-Id-Version: Orca Slicer\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-22 02:24+0800\n"
|
||||
"PO-Revision-Date: 2026-05-28 22:39+0700\n"
|
||||
"Last-Translator: Icezaza\n"
|
||||
"Language-Team: Thai\n"
|
||||
"Language: th\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Codex Thai translation bootstrap\n"
|
||||
|
||||
msgid "right"
|
||||
msgstr "ขวา"
|
||||
@@ -9608,10 +9620,10 @@ msgid "Normal"
|
||||
msgstr "ปกติ"
|
||||
|
||||
msgid "Resonance Compensation"
|
||||
msgstr "การชดเชยเสียงสะท้อน"
|
||||
msgstr "การชดเชยการสั่นพ้อง"
|
||||
|
||||
msgid "Resonance Avoidance Speed"
|
||||
msgstr "ความเร็วการหลีกเลี่ยงเสียงสะท้อน"
|
||||
msgstr "ความเร็วการหลีกเลี่ยงการสั่นพ้อง"
|
||||
|
||||
msgid "Frequency"
|
||||
msgstr "ความถี่"
|
||||
@@ -9758,7 +9770,7 @@ msgid "Continue"
|
||||
msgstr "ดำเนินการต่อ"
|
||||
|
||||
msgid "Back"
|
||||
msgstr "ย้อนกลับ"
|
||||
msgstr "หลัง"
|
||||
|
||||
msgid "Don't warn again for this preset"
|
||||
msgstr "ไม่ต้องเตือนอีกสำหรับค่าที่กำหนดล่วงหน้านี้"
|
||||
@@ -9799,7 +9811,7 @@ msgid "Transfer"
|
||||
msgstr "โอนย้าย"
|
||||
|
||||
msgid "Don't save"
|
||||
msgstr "อย่าบันทึก"
|
||||
msgstr "ไม่บันทึก"
|
||||
|
||||
msgid "Discard"
|
||||
msgstr "ทิ้ง"
|
||||
@@ -11852,10 +11864,10 @@ msgid ""
|
||||
msgstr "สร้างเส้นทางรอบขอบเพิ่มไส้ในเหนือส่วนยื่นสูงชันและพื้นที่ที่ไม่สามารถทอดสมอสะพานได้"
|
||||
|
||||
msgid "Reverse on even"
|
||||
msgstr "ย้อนกลับในคู่"
|
||||
msgstr "กลับด้านหน้าเลขคู่"
|
||||
|
||||
msgid "Overhang reversal"
|
||||
msgstr "การกลับรายการยื่นออกมา"
|
||||
msgstr "การกลับด้านส่วนยื่นออกมา"
|
||||
|
||||
msgid ""
|
||||
"Extrude perimeters that have a part over an overhang in the reverse "
|
||||
@@ -11867,7 +11879,7 @@ msgid ""
|
||||
msgstr "รีดเส้นรอบวงที่มีส่วนยื่นออกมาในทิศทางย้อนกลับบนชั้นคู่ รูปแบบการสลับนี้สามารถปรับปรุงระยะยื่นที่สูงชันได้อย่างมาก\n\nการตั้งค่านี้ยังช่วยลดการบิดงอของชิ้นส่วนเนื่องจากการลดความเค้นในผนังชิ้นส่วนอีกด้วย"
|
||||
|
||||
msgid "Reverse only internal perimeters"
|
||||
msgstr "ย้อนกลับเฉพาะขอบเขตภายในเท่านั้น"
|
||||
msgstr "กลับด้านเฉพาะขอบเขตภายในเท่านั้น"
|
||||
|
||||
msgid ""
|
||||
"Apply the reverse perimeters logic only on internal perimeters.\n"
|
||||
@@ -11902,7 +11914,7 @@ msgid "Sacrificial layer"
|
||||
msgstr "ชั้นบูชายัญ"
|
||||
|
||||
msgid "Reverse threshold"
|
||||
msgstr "เกณฑ์ย้อนกลับ"
|
||||
msgstr "เกณฑ์การกลับด้าน"
|
||||
|
||||
msgid "Overhang reversal threshold"
|
||||
msgstr "เกณฑ์การกลับรายการส่วนเกิน"
|
||||
@@ -14395,25 +14407,25 @@ msgid "Maximum acceleration for travel (M204 T), it only applies to Marlin 2."
|
||||
msgstr "อัตราเร่งสูงสุดสำหรับการเดินทาง (M204 T) ใช้กับ Marlin 2 เท่านั้น"
|
||||
|
||||
msgid "Resonance avoidance"
|
||||
msgstr "การหลีกเลี่ยงเสียงสะท้อน"
|
||||
msgstr "การหลีกเลี่ยงการสั่นพ้อง"
|
||||
|
||||
msgid ""
|
||||
"By reducing the speed of the outer wall to avoid the resonance zone of the "
|
||||
"printer, ringing on the surface of the model are avoided.\n"
|
||||
"Please turn this option off when testing ringing."
|
||||
msgstr "โดยการลดความเร็วของผนังด้านนอกเพื่อหลีกเลี่ยงโซนเสียงสะท้อนของเครื่องพิมพ์ หลีกเลี่ยงเสียงกริ่งบนพื้นผิวของแบบจำลอง\nโปรดปิดตัวเลือกนี้เมื่อทดสอบเสียงเรียกเข้า"
|
||||
msgstr "โดยการลดความเร็วของผนังด้านนอกเพื่อหลีกเลี่ยงโซนสั่นพ้องของเครื่องพิมพ์ หลีกเลี่ยงรอยคลื่นบนพื้นผิวของโมเดล\nโปรดปิดตัวเลือกนี้เมื่อทดสอบการสั่นพ้อง"
|
||||
|
||||
msgid "Min"
|
||||
msgstr "นาที"
|
||||
|
||||
msgid "Minimum speed of resonance avoidance."
|
||||
msgstr "ความเร็วขั้นต่ำของการหลีกเลี่ยงเสียงสะท้อน"
|
||||
msgstr "ความเร็วขั้นต่ำของการหลีกเลี่ยงเสียงการสั่นพ้อง"
|
||||
|
||||
msgid "Max"
|
||||
msgstr "สูงสุด"
|
||||
|
||||
msgid "Maximum speed of resonance avoidance."
|
||||
msgstr "ความเร็วสูงสุดของการหลีกเลี่ยงเสียงสะท้อน"
|
||||
msgstr "ความเร็วสูงสุดของการหลีกเลี่ยงการสั่นพ้อง"
|
||||
|
||||
msgid "Emit input shaping"
|
||||
msgstr "ปล่อยรูปร่างอินพุต"
|
||||
|
||||
BIN
resources/fonts/Sarabun-Medium.ttf
Normal file
BIN
resources/fonts/Sarabun-Medium.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Sarabun-SemiBold.ttf
Normal file
BIN
resources/fonts/Sarabun-SemiBold.ttf
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Elegoo",
|
||||
"version": "02.04.00.00",
|
||||
"version": "02.04.00.02",
|
||||
"force_update": "0",
|
||||
"description": "Elegoo configurations",
|
||||
"machine_model_list": [
|
||||
@@ -286,6 +286,10 @@
|
||||
"name": "0.40mm Standard @Elegoo Neptune 0.8 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.40mm Standard @Elegoo Neptune 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.50mm Standard @Elegoo Giga 1.0 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.50mm Standard @Elegoo Giga 1.0 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.50mm Standard @Elegoo N3Max 1.0 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.50mm Standard @Elegoo N3Max 1.0 nozzle.json"
|
||||
@@ -314,6 +318,42 @@
|
||||
"name": "0.50mm Standard @Elegoo N4Pro 1.0 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.50mm Standard @Elegoo N4Pro 1.0 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.08mm Optimal @Elegoo C 0.2 nozzle",
|
||||
"sub_path": "process/EC/0.08mm Optimal @Elegoo C 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Draft @Elegoo C 0.2 nozzle",
|
||||
"sub_path": "process/EC/0.12mm Draft @Elegoo C 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.14mm Extra Draft @Elegoo C 0.2 nozzle",
|
||||
"sub_path": "process/EC/0.14mm Extra Draft @Elegoo C 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.08mm Optimal @Elegoo CC 0.2 nozzle",
|
||||
"sub_path": "process/ECC/0.08mm Optimal @Elegoo CC 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Draft @Elegoo CC 0.2 nozzle",
|
||||
"sub_path": "process/ECC/0.12mm Draft @Elegoo CC 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.14mm Extra Draft @Elegoo CC 0.2 nozzle",
|
||||
"sub_path": "process/ECC/0.14mm Extra Draft @Elegoo CC 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.08mm Optimal @Elegoo CC2 0.2 nozzle",
|
||||
"sub_path": "process/ECC2/0.08mm Optimal @Elegoo CC2 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Draft @Elegoo CC2 0.2 nozzle",
|
||||
"sub_path": "process/ECC2/0.12mm Draft @Elegoo CC2 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.14mm Extra Draft @Elegoo CC2 0.2 nozzle",
|
||||
"sub_path": "process/ECC2/0.14mm Extra Draft @Elegoo CC2 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.08mm Optimal @Elegoo N3Max 0.2 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.08mm Optimal @Elegoo N3Max 0.2 nozzle.json"
|
||||
@@ -370,6 +410,322 @@
|
||||
"name": "0.12mm Draft @Elegoo N4Pro 0.2 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.12mm Draft @Elegoo N4Pro 0.2 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo C 0.4 nozzle",
|
||||
"sub_path": "process/EC/0.12mm Fine @Elegoo C 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo C 0.4 nozzle",
|
||||
"sub_path": "process/EC/0.16mm Optimal @Elegoo C 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo C 0.4 nozzle",
|
||||
"sub_path": "process/EC/0.20mm Strength @Elegoo C 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo C 0.4 nozzle",
|
||||
"sub_path": "process/EC/0.24mm Draft @Elegoo C 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo C 0.4 nozzle",
|
||||
"sub_path": "process/EC/0.28mm Extra Draft @Elegoo C 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo CC 0.4 nozzle",
|
||||
"sub_path": "process/ECC/0.12mm Fine @Elegoo CC 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo CC 0.4 nozzle",
|
||||
"sub_path": "process/ECC/0.16mm Optimal @Elegoo CC 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo CC 0.4 nozzle",
|
||||
"sub_path": "process/ECC/0.20mm Strength @Elegoo CC 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo CC 0.4 nozzle",
|
||||
"sub_path": "process/ECC/0.24mm Draft @Elegoo CC 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo CC 0.4 nozzle",
|
||||
"sub_path": "process/ECC/0.28mm Extra Draft @Elegoo CC 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo CC2 0.4 nozzle",
|
||||
"sub_path": "process/ECC2/0.12mm Fine @Elegoo CC2 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo CC2 0.4 nozzle",
|
||||
"sub_path": "process/ECC2/0.16mm Optimal @Elegoo CC2 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo CC2 0.4 nozzle",
|
||||
"sub_path": "process/ECC2/0.20mm Strength @Elegoo CC2 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo CC2 0.4 nozzle",
|
||||
"sub_path": "process/ECC2/0.24mm Draft @Elegoo CC2 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo CC2 0.4 nozzle",
|
||||
"sub_path": "process/ECC2/0.28mm Extra Draft @Elegoo CC2 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo Giga 0.4 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.16mm Optimal @Elegoo Giga 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo Giga 0.4 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.20mm Strength @Elegoo Giga 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo Giga 0.4 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.24mm Draft @Elegoo Giga 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo Giga 0.4 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.28mm Extra Draft @Elegoo Giga 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo N3Max 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.12mm Fine @Elegoo N3Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo N3Max 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.16mm Optimal @Elegoo N3Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo N3Max 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.20mm Strength @Elegoo N3Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo N3Max 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.24mm Draft @Elegoo N3Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo N3Max 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.28mm Extra Draft @Elegoo N3Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo N3Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.12mm Fine @Elegoo N3Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo N3Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.16mm Optimal @Elegoo N3Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo N3Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.20mm Strength @Elegoo N3Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo N3Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.24mm Draft @Elegoo N3Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo N3Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.28mm Extra Draft @Elegoo N3Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo N3Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.12mm Fine @Elegoo N3Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo N3Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.16mm Optimal @Elegoo N3Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo N3Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.20mm Strength @Elegoo N3Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo N3Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.24mm Draft @Elegoo N3Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo N3Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.28mm Extra Draft @Elegoo N3Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo N4 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.12mm Fine @Elegoo N4 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo N4 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.16mm Optimal @Elegoo N4 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo N4 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.20mm Strength @Elegoo N4 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo N4 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.24mm Draft @Elegoo N4 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo N4 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.28mm Extra Draft @Elegoo N4 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo N4Max 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.12mm Fine @Elegoo N4Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo N4Max 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.16mm Optimal @Elegoo N4Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo N4Max 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.20mm Strength @Elegoo N4Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo N4Max 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.24mm Draft @Elegoo N4Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo N4Max 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.28mm Extra Draft @Elegoo N4Max 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo N4Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.12mm Fine @Elegoo N4Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo N4Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.16mm Optimal @Elegoo N4Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo N4Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.20mm Strength @Elegoo N4Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo N4Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.24mm Draft @Elegoo N4Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo N4Plus 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.28mm Extra Draft @Elegoo N4Plus 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo N4Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.12mm Fine @Elegoo N4Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo N4Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.16mm Optimal @Elegoo N4Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo N4Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.20mm Strength @Elegoo N4Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo N4Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.24mm Draft @Elegoo N4Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo N4Pro 0.4 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.28mm Extra Draft @Elegoo N4Pro 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.12mm Fine @Elegoo Neptune 0.4 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.12mm Fine @Elegoo Neptune 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Optimal @Elegoo Neptune 0.4 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.16mm Optimal @Elegoo Neptune 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.20mm Strength @Elegoo Neptune 0.4 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.20mm Strength @Elegoo Neptune 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Draft @Elegoo Neptune 0.4 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.24mm Draft @Elegoo Neptune 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.28mm Extra Draft @Elegoo Neptune 0.4 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.28mm Extra Draft @Elegoo Neptune 0.4 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.18mm Fine @Elegoo C 0.6 nozzle",
|
||||
"sub_path": "process/EC/0.18mm Fine @Elegoo C 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Optimal @Elegoo C 0.6 nozzle",
|
||||
"sub_path": "process/EC/0.24mm Optimal @Elegoo C 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.30mm Strength @Elegoo C 0.6 nozzle",
|
||||
"sub_path": "process/EC/0.30mm Strength @Elegoo C 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.36mm Draft @Elegoo C 0.6 nozzle",
|
||||
"sub_path": "process/EC/0.36mm Draft @Elegoo C 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.42mm Extra Draft @Elegoo C 0.6 nozzle",
|
||||
"sub_path": "process/EC/0.42mm Extra Draft @Elegoo C 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.18mm Fine @Elegoo CC 0.6 nozzle",
|
||||
"sub_path": "process/ECC/0.18mm Fine @Elegoo CC 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Optimal @Elegoo CC 0.6 nozzle",
|
||||
"sub_path": "process/ECC/0.24mm Optimal @Elegoo CC 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.30mm Strength @Elegoo CC 0.6 nozzle",
|
||||
"sub_path": "process/ECC/0.30mm Strength @Elegoo CC 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.36mm Draft @Elegoo CC 0.6 nozzle",
|
||||
"sub_path": "process/ECC/0.36mm Draft @Elegoo CC 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.42mm Extra Draft @Elegoo CC 0.6 nozzle",
|
||||
"sub_path": "process/ECC/0.42mm Extra Draft @Elegoo CC 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.18mm Fine @Elegoo CC2 0.6 nozzle",
|
||||
"sub_path": "process/ECC2/0.18mm Fine @Elegoo CC2 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Optimal @Elegoo CC2 0.6 nozzle",
|
||||
"sub_path": "process/ECC2/0.24mm Optimal @Elegoo CC2 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.30mm Strength @Elegoo CC2 0.6 nozzle",
|
||||
"sub_path": "process/ECC2/0.30mm Strength @Elegoo CC2 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.36mm Draft @Elegoo CC2 0.6 nozzle",
|
||||
"sub_path": "process/ECC2/0.36mm Draft @Elegoo CC2 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.42mm Extra Draft @Elegoo CC2 0.6 nozzle",
|
||||
"sub_path": "process/ECC2/0.42mm Extra Draft @Elegoo CC2 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.18mm Fine @Elegoo Giga 0.6 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.18mm Fine @Elegoo Giga 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Optimal @Elegoo Giga 0.6 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.24mm Optimal @Elegoo Giga 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.30mm Strength @Elegoo Giga 0.6 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.30mm Strength @Elegoo Giga 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.36mm Draft @Elegoo Giga 0.6 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.36mm Draft @Elegoo Giga 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.42mm Extra Draft @Elegoo Giga 0.6 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.42mm Extra Draft @Elegoo Giga 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Optimal @Elegoo N3Max 0.6 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.24mm Optimal @Elegoo N3Max 0.6 nozzle.json"
|
||||
@@ -454,6 +810,82 @@
|
||||
"name": "0.42mm Extra Draft @Elegoo N4Pro 0.6 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.42mm Extra Draft @Elegoo N4Pro 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Optimal @Elegoo Neptune 0.6 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.24mm Optimal @Elegoo Neptune 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.36mm Draft @Elegoo Neptune 0.6 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.36mm Draft @Elegoo Neptune 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.42mm Extra Draft @Elegoo Neptune 0.6 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.42mm Extra Draft @Elegoo Neptune 0.6 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Extra Fine @Elegoo C 0.8 nozzle",
|
||||
"sub_path": "process/EC/0.16mm Extra Fine @Elegoo C 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Fine @Elegoo C 0.8 nozzle",
|
||||
"sub_path": "process/EC/0.24mm Fine @Elegoo C 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.32mm Optimal @Elegoo C 0.8 nozzle",
|
||||
"sub_path": "process/EC/0.32mm Optimal @Elegoo C 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.48mm Draft @Elegoo C 0.8 nozzle",
|
||||
"sub_path": "process/EC/0.48mm Draft @Elegoo C 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Extra Fine @Elegoo CC 0.8 nozzle",
|
||||
"sub_path": "process/ECC/0.16mm Extra Fine @Elegoo CC 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Fine @Elegoo CC 0.8 nozzle",
|
||||
"sub_path": "process/ECC/0.24mm Fine @Elegoo CC 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.32mm Optimal @Elegoo CC 0.8 nozzle",
|
||||
"sub_path": "process/ECC/0.32mm Optimal @Elegoo CC 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.48mm Draft @Elegoo CC 0.8 nozzle",
|
||||
"sub_path": "process/ECC/0.48mm Draft @Elegoo CC 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.16mm Extra Fine @Elegoo CC2 0.8 nozzle",
|
||||
"sub_path": "process/ECC2/0.16mm Extra Fine @Elegoo CC2 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Fine @Elegoo CC2 0.8 nozzle",
|
||||
"sub_path": "process/ECC2/0.24mm Fine @Elegoo CC2 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.32mm Optimal @Elegoo CC2 0.8 nozzle",
|
||||
"sub_path": "process/ECC2/0.32mm Optimal @Elegoo CC2 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.48mm Draft @Elegoo CC2 0.8 nozzle",
|
||||
"sub_path": "process/ECC2/0.48mm Draft @Elegoo CC2 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Fine @Elegoo Giga 0.8 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.24mm Fine @Elegoo Giga 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.32mm Optimal @Elegoo Giga 0.8 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.32mm Optimal @Elegoo Giga 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.48mm Draft @Elegoo Giga 0.8 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.48mm Draft @Elegoo Giga 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.56mm Extra Draft @Elegoo Giga 0.8 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.56mm Extra Draft @Elegoo Giga 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Fine @Elegoo N3Max 0.8 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.24mm Fine @Elegoo N3Max 0.8 nozzle.json"
|
||||
@@ -538,6 +970,26 @@
|
||||
"name": "0.48mm Draft @Elegoo N4Pro 0.8 nozzle",
|
||||
"sub_path": "process/EN4SERIES/0.48mm Draft @Elegoo N4Pro 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.24mm Fine @Elegoo Neptune 0.8 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.24mm Fine @Elegoo Neptune 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.32mm Optimal @Elegoo Neptune 0.8 nozzle",
|
||||
"sub_path": "process/EN2SERIES/0.32mm Optimal @Elegoo Neptune 0.8 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.30mm Fine @Elegoo Giga 1.0 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.30mm Fine @Elegoo Giga 1.0 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.40mm Optimal @Elegoo Giga 1.0 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.40mm Optimal @Elegoo Giga 1.0 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.60mm Draft @Elegoo Giga 1.0 nozzle",
|
||||
"sub_path": "process/EOSGIGA/0.60mm Draft @Elegoo Giga 1.0 nozzle.json"
|
||||
},
|
||||
{
|
||||
"name": "0.30mm Fine @Elegoo N3Max 1.0 nozzle",
|
||||
"sub_path": "process/EN3SERIES/0.30mm Fine @Elegoo N3Max 1.0 nozzle.json"
|
||||
|
||||
@@ -68,6 +68,258 @@
|
||||
"name": "fdm_filament_pla",
|
||||
"sub_path": "filament/base/fdm_filament_pla.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA @base",
|
||||
"sub_path": "filament/FilAr/FilAr PLA @base.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Bronce",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Bronce.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Gris Plata",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Gris Plata.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Cobre",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Cobre.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Titanio",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Titanio.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Tabaco",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Tabaco.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Cafe con Leche",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Cafe con Leche.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Manteca",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Manteca.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Marron Oxido",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Marron Oxido.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Carpincho",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Carpincho.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Rosa Amaranto",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Rosa Amaranto.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Rosa Flamenco",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Rosa Flamenco.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Piel",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Piel.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Verde FilAr",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Verde FilAr.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Verde Manzana",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Verde Manzana.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Verde Pixel",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Verde Pixel.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Verde Oliva",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Verde Oliva.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Blanco Antartida",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Blanco Antartida.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Blanco Calido",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Blanco Calido.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Negro Azabache",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Negro Azabache.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Naranja Tigre",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Naranja Tigre.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Rojo de Carreras",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Rojo de Carreras.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Amarillo Lirio",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Amarillo Lirio.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Violeta Jacaranda",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Violeta Jacaranda.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Gris Pizarra",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Gris Pizarra.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Gris Ceniza",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Gris Ceniza.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Azul Francia",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Azul Francia.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Celeste Cielo",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Celeste Cielo.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Oro",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Oro.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA Dorado",
|
||||
"sub_path": "filament/FilAr/FilAr PLA Dorado.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate @base",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate @base.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Amarillo",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Amarillo.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Azul",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Azul.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Beige",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Beige.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Blanco",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Blanco.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Bordo",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Bordo.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Celeste Cielo",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Celeste Cielo.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Chocolate",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Chocolate.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Gris",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Gris.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Marron",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Marron.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Naranja",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Naranja.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Negro",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Negro.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Piel",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Piel.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Rojo",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Rojo.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Rosa",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Rosa.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Uva",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Uva.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Verde",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Verde.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PLA-mate Violeta",
|
||||
"sub_path": "filament/FilAr/FilAr PLA-mate Violeta.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG @base",
|
||||
"sub_path": "filament/FilAr/FilAr PETG @base.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Amarillo Lima",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Amarillo Lima.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Amarillo Radiante",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Amarillo Radiante.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Azul Boreal",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Azul Boreal.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Azul Francia",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Azul Francia.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Azul Imperial",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Azul Imperial.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Blanco Antartida",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Blanco Antartida.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Cian",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Cian.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Coral",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Coral.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Cristal",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Cristal.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Gris Ceniza",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Gris Ceniza.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Gris Plata",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Gris Plata.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Magenta",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Magenta.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Negro Azabache",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Negro Azabache.json"
|
||||
},
|
||||
{
|
||||
"name": "FilAr PETG Rojo Carmesi",
|
||||
"sub_path": "filament/FilAr/FilAr PETG Rojo Carmesi.json"
|
||||
},
|
||||
{
|
||||
"name": "fdm_filament_pp",
|
||||
"sub_path": "filament/base/fdm_filament_pp.json"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG @base",
|
||||
"inherits": "fdm_filament_pet",
|
||||
"from": "system",
|
||||
"filament_id": "FILARB03",
|
||||
"instantiation": "false",
|
||||
"filament_type": [
|
||||
"PETG"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"filament_diameter": [
|
||||
"1.75"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.27"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.95"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"240"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"245"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"230"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"250"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"78"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"78"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"78"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"78"
|
||||
],
|
||||
"fan_min_speed": [
|
||||
"40"
|
||||
],
|
||||
"fan_max_speed": [
|
||||
"60"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Amarillo Lima",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0047",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#C7E03A"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Amarillo Radiante",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0048",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FFD400"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Azul Boreal",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0049",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#1B4F9C"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Azul Francia",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0050",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#0033A0"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Azul Imperial",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0051",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#14306B"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Blanco Antartida",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0052",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#F7FAFC"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Cian",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0053",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#00B7D4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Coral",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0054",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FF6F5E"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Cristal",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0055",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#E8F0EC"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Gris Ceniza",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0056",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#B4B6B1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Gris Plata",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0057",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#8C8686"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Magenta",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0058",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#B5174B"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Negro Azabache",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0059",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#000000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PETG Rojo Carmesi",
|
||||
"inherits": "FilAr PETG @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0060",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#8C1020"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA @base",
|
||||
"inherits": "fdm_filament_pla",
|
||||
"from": "system",
|
||||
"filament_id": "FILARB01",
|
||||
"instantiation": "false",
|
||||
"filament_type": [
|
||||
"PLA"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"filament_diameter": [
|
||||
"1.75"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.24"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.98"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"210"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"215"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"200"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"220"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"60"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"60"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"60"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"60"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Amarillo Lirio",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0022",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FFD52B"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Azul Francia",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0026",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#0000FF"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Blanco Antartida",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0017",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#F7FAFC"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Blanco Calido",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0018",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FFF8E7"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Bronce",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0001",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#AD8428"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Cafe con Leche",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0006",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#E0B269"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Carpincho",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0009",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#E8DFC1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Celeste Cielo",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0027",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#A1EBFF"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Cobre",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0003",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#87421E"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Dorado",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0029",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#A67F00"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Gris Ceniza",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0025",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#B4B6B1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Gris Pizarra",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0024",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#4B4B4B"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Gris Plata",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0002",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#8C8686"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Manteca",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0007",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FFF5AB"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Marron Oxido",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0008",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#5E190E"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Naranja Tigre",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0020",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FC5017"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Negro Azabache",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0019",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#000000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Oro",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0028",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FFD700"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Piel",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0012",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FADDAC"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Rojo de Carreras",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0021",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FA0F0F"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Rosa Amaranto",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0010",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FA4164"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Rosa Flamenco",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0011",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#FFC7C2"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Tabaco",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0005",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#7D5429"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Titanio",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0004",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#878681"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Verde FilAr",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0013",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#036D6B"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Verde Manzana",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0014",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#D4FF38"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Verde Oliva",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0016",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#595900"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Verde Pixel",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0015",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#02E32F"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA Violeta Jacaranda",
|
||||
"inherits": "FilAr PLA @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0023",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#9342C9"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate @base",
|
||||
"inherits": "fdm_filament_pla",
|
||||
"from": "system",
|
||||
"filament_id": "FILARB02",
|
||||
"instantiation": "false",
|
||||
"filament_type": [
|
||||
"PLA"
|
||||
],
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"filament_diameter": [
|
||||
"1.75"
|
||||
],
|
||||
"filament_density": [
|
||||
"1.24"
|
||||
],
|
||||
"filament_flow_ratio": [
|
||||
"0.98"
|
||||
],
|
||||
"nozzle_temperature": [
|
||||
"200"
|
||||
],
|
||||
"nozzle_temperature_initial_layer": [
|
||||
"205"
|
||||
],
|
||||
"nozzle_temperature_range_low": [
|
||||
"195"
|
||||
],
|
||||
"nozzle_temperature_range_high": [
|
||||
"210"
|
||||
],
|
||||
"hot_plate_temp": [
|
||||
"60"
|
||||
],
|
||||
"hot_plate_temp_initial_layer": [
|
||||
"60"
|
||||
],
|
||||
"textured_plate_temp": [
|
||||
"60"
|
||||
],
|
||||
"textured_plate_temp_initial_layer": [
|
||||
"60"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Amarillo",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0030",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#E8C547"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Azul",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0031",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#2D5DA8"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Beige",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0032",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#D8C4A0"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Blanco",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0033",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#F2F2EC"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Bordo",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0034",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#6E1A2B"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Celeste Cielo",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0035",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#9DC8E8"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Chocolate",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0036",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#4A2E22"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Gris",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0037",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#7E7E7E"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Marron",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0038",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#6B4226"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Naranja",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0039",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#E0662A"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Negro",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0040",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#1A1A1A"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Piel",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0041",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#E8C9A8"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Rojo",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0042",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#B5302E"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Rosa",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0043",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#E07A9B"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Uva",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0044",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#5B2A6B"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Verde",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0045",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#3A7D44"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "filament",
|
||||
"name": "FilAr PLA-mate Violeta",
|
||||
"inherits": "FilAr PLA-mate @base",
|
||||
"from": "system",
|
||||
"filament_id": "FILAR0046",
|
||||
"instantiation": "true",
|
||||
"filament_vendor": [
|
||||
"FilAr"
|
||||
],
|
||||
"default_filament_colour": [
|
||||
"#6B3FA0"
|
||||
]
|
||||
}
|
||||
@@ -1339,16 +1339,6 @@ int CLI::run(int argc, char **argv)
|
||||
//BBS: remove GCodeViewer as separate APP logic
|
||||
//params.start_as_gcodeviewer = start_as_gcodeviewer;
|
||||
|
||||
// UI automation server (opt-in). --automation-server enables it;
|
||||
// --automation-server-port overrides the default 13619.
|
||||
ConfigOptionBool* automation_server_option = m_config.option<ConfigOptionBool>("automation_server");
|
||||
if (automation_server_option && automation_server_option->value) {
|
||||
ConfigOptionInt* automation_port_option = m_config.option<ConfigOptionInt>("automation_server_port");
|
||||
int port = (automation_port_option && automation_port_option->value > 0) ? automation_port_option->value : 13619;
|
||||
params.automation_port = port;
|
||||
BOOST_LOG_TRIVIAL(warning) << "UI automation server requested on port " << params.automation_port;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "begin to launch OrcaSlicer GUI soon";
|
||||
return Slic3r::GUI::GUI_Run(params);
|
||||
#else // SLIC3R_GUI
|
||||
@@ -1367,6 +1357,10 @@ int CLI::run(int argc, char **argv)
|
||||
else {
|
||||
set_logging_level(2);
|
||||
}
|
||||
const ConfigOptionString* opt_logfile = m_config.opt<ConfigOptionString>("logfile");
|
||||
if (opt_logfile) {
|
||||
set_logging_file(opt_logfile->value);
|
||||
}
|
||||
|
||||
global_begin_time = (long long)Slic3r::Utils::get_current_time_utc();
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("cli mode, Current OrcaSlicer Version %1%")%SoftFever_VERSION;
|
||||
|
||||
@@ -266,10 +266,44 @@ std::vector<WaveSeed> wave_seeds(
|
||||
//(front.z() < 0 && back.z() < 0));
|
||||
// Hope that at least one end of an open polyline is clipped by the boundary, thus an intersection point is created.
|
||||
(front.z() < 0 || back.z() < 0));
|
||||
// However, with complex geometry, both endpoints may coincide with existing polygon
|
||||
// vertices (z >= 0), which is handled below.
|
||||
|
||||
if (front != back && front.z() >= 0 && back.z() >= 0) {
|
||||
// Very rare case when both endpoints intersect boundary ExPolygons in existing points.
|
||||
// So the ZFillFunction callback hasn't been called.
|
||||
// Both endpoints coincide with existing polygon vertices, so the
|
||||
// ZFillFunction callback was never called. With complex geometry
|
||||
// this is common because source and boundary contours share many
|
||||
// vertices. Determine src_id / boundary_id from Z coordinates
|
||||
// (and fall back to an AABB-tree point-in-polygon test when a
|
||||
// boundary ID is not directly available).
|
||||
coord_t src_z = -1, boundary_z = -1;
|
||||
// Scan all path points for the information we need.
|
||||
for (const ClipperLib_Z::IntPoint &point : path) {
|
||||
if (point.z() >= idx_boundary_end && point.z() < idx_src_end && src_z < 0)
|
||||
src_z = point.z();
|
||||
else if (point.z() >= idx_boundary_begin && point.z() < idx_boundary_end && boundary_z < 0)
|
||||
boundary_z = point.z();
|
||||
if (src_z >= 0 && boundary_z >= 0)
|
||||
break;
|
||||
}
|
||||
if (src_z >= 0) {
|
||||
uint32_t src_id = uint32_t(src_z - idx_boundary_end);
|
||||
if (boundary_z >= 0) {
|
||||
out.push_back({ src_id, uint32_t(boundary_z - 1), ClipperZUtils::from_zpath(path) });
|
||||
} else {
|
||||
// Source ID known but boundary unknown – use AABB tree.
|
||||
if (aabb_tree.empty())
|
||||
aabb_tree = build_aabb_tree_over_expolygons(boundary);
|
||||
int boundary_id = sample_in_expolygons(aabb_tree, boundary, Point(front.x(), front.y()));
|
||||
if (boundary_id >= 0)
|
||||
out.push_back({ src_id, uint32_t(boundary_id), ClipperZUtils::from_zpath(path) });
|
||||
}
|
||||
++ iseed;
|
||||
continue;
|
||||
}
|
||||
// Unable to determine source ID – drop the segment.
|
||||
continue;
|
||||
} else
|
||||
if (front == back && (front.z() < idx_boundary_end)) {
|
||||
|
||||
@@ -326,6 +326,9 @@ void AppConfig::set_defaults()
|
||||
if (get("developer_mode").empty())
|
||||
set_bool("developer_mode", false);
|
||||
|
||||
if (get("show_unsupported_presets").empty())
|
||||
set_bool("show_unsupported_presets", false);
|
||||
|
||||
if (get("enable_ssl_for_mqtt").empty())
|
||||
set_bool("enable_ssl_for_mqtt", true);
|
||||
|
||||
|
||||
@@ -935,10 +935,13 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
params.fixed_angle = !region_config.solid_infill_rotate_template.value.empty();
|
||||
}
|
||||
params.bridge_angle = float(surface.bridge_angle);
|
||||
|
||||
|
||||
// ORCA: Align infill angle to model
|
||||
float align_offset = 0.f;
|
||||
if (region_config.align_infill_direction_to_model) {
|
||||
auto m = layer.object()->trafo().matrix();
|
||||
params.angle += atan2((float) m(1, 0), (float) m(0, 0));
|
||||
align_offset = atan2((float)m(1, 0), (float)m(0, 0));
|
||||
params.angle += align_offset;
|
||||
}
|
||||
|
||||
// Calculate the actual flow we'll be using for this infill.
|
||||
@@ -1024,6 +1027,7 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
if (fill.region_id == size_t(-1)) {
|
||||
fill.region_id = region_id;
|
||||
fill.surface = surface;
|
||||
fill.surface.bridge_angle = params->bridge_angle;
|
||||
fill.expolygons.emplace_back(std::move(fill.surface.expolygon));
|
||||
//BBS
|
||||
fill.region_id_group.push_back(region_id);
|
||||
@@ -1578,7 +1582,7 @@ void Layer::make_ironing()
|
||||
if (ironing_params.extruder != -1) {
|
||||
//TODO just_infill is currently not used.
|
||||
ironing_params.just_infill = false;
|
||||
// Get filament-specific overrides if configured, otherwise use default values
|
||||
// ORCA: Get filament-specific overrides if configured, otherwise use process values
|
||||
size_t extruder_idx = ironing_params.extruder - 1;
|
||||
ironing_params.line_spacing = (!config.filament_ironing_spacing.is_nil(extruder_idx)
|
||||
? config.filament_ironing_spacing.get_at(extruder_idx)
|
||||
@@ -1592,7 +1596,12 @@ void Layer::make_ironing()
|
||||
ironing_params.speed = (!config.filament_ironing_speed.is_nil(extruder_idx)
|
||||
? config.filament_ironing_speed.get_at(extruder_idx)
|
||||
: config.ironing_speed);
|
||||
ironing_params.angle = (config.ironing_angle_fixed ? 0 : calculate_infill_rotation_angle(this->object(), this->id(), config.solid_infill_direction.value, config.solid_infill_rotate_template.value)) + config.ironing_angle * M_PI / 180.;
|
||||
double ironing_angle = (config.ironing_angle_fixed ? 0 : calculate_infill_rotation_angle(this->object(), this->id(), config.solid_infill_direction.value, config.solid_infill_rotate_template.value)) + config.ironing_angle * M_PI / 180.;
|
||||
if (config.align_infill_direction_to_model) {
|
||||
auto m = this->object()->trafo().matrix();
|
||||
ironing_angle += atan2((double)m(1, 0), (double)m(0, 0));
|
||||
}
|
||||
ironing_params.angle = ironing_angle;
|
||||
ironing_params.fixed_angle = config.ironing_angle_fixed || !config.solid_infill_rotate_template.value.empty();
|
||||
ironing_params.pattern = config.ironing_pattern;
|
||||
ironing_params.layerm = layerm;
|
||||
|
||||
@@ -49,6 +49,8 @@ static inline FlowRole opt_key_to_flow_role(const std::string &opt_key)
|
||||
return frInfill;
|
||||
else if (opt_key == "internal_solid_infill_line_width")
|
||||
return frSolidInfill;
|
||||
else if (opt_key == "bridge_line_width")
|
||||
return frSolidInfill;
|
||||
else if (opt_key == "top_surface_line_width")
|
||||
return frTopSolidInfill;
|
||||
else if (opt_key == "support_line_width")
|
||||
@@ -67,6 +69,26 @@ double Flow::extrusion_width(const std::string& opt_key, const ConfigOptionFloat
|
||||
{
|
||||
assert(opt != nullptr);
|
||||
|
||||
auto opt_nozzle_diameters = config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
if (opt_nozzle_diameters == nullptr)
|
||||
throw_on_missing_variable(opt_key, "nozzle_diameter");
|
||||
const float nozzle_diameter = float(opt_nozzle_diameters->get_at(first_printing_extruder));
|
||||
|
||||
if (opt_key == "bridge_line_width") {
|
||||
if (opt->percent) {
|
||||
const double bridge_width = opt->get_abs_value(nozzle_diameter);
|
||||
if (bridge_width > 0.)
|
||||
return bridge_width;
|
||||
} else if (opt->value > 0.) {
|
||||
return opt->value;
|
||||
}
|
||||
|
||||
opt = config.option<ConfigOptionFloatOrPercent>("internal_solid_infill_line_width");
|
||||
if (opt == nullptr)
|
||||
throw_on_missing_variable(opt_key, "internal_solid_infill_line_width");
|
||||
return extrusion_width("internal_solid_infill_line_width", opt, config, first_printing_extruder);
|
||||
}
|
||||
|
||||
#if 0
|
||||
// This is the logic used for skit / brim, but not for the rest of the 1st layer.
|
||||
if (opt->value == 0. && first_layer) {
|
||||
@@ -84,17 +106,13 @@ double Flow::extrusion_width(const std::string& opt_key, const ConfigOptionFloat
|
||||
throw_on_missing_variable(opt_key, "line_width");
|
||||
}
|
||||
|
||||
auto opt_nozzle_diameters = config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
if (opt_nozzle_diameters == nullptr)
|
||||
throw_on_missing_variable(opt_key, "nozzle_diameter");
|
||||
|
||||
if (opt->percent) {
|
||||
return opt->get_abs_value(float(opt_nozzle_diameters->get_at(first_printing_extruder)));
|
||||
return opt->get_abs_value(nozzle_diameter);
|
||||
}
|
||||
|
||||
if (opt->value == 0.) {
|
||||
// If user left option to 0, calculate a sane default width.
|
||||
return auto_extrusion_width(opt_key_to_flow_role(opt_key), float(opt_nozzle_diameters->get_at(first_printing_extruder)));
|
||||
return auto_extrusion_width(opt_key_to_flow_role(opt_key), nozzle_diameter);
|
||||
}
|
||||
|
||||
return opt->value;
|
||||
|
||||
@@ -34,16 +34,26 @@ Flow LayerRegion::bridging_flow(FlowRole role, bool thick_bridge) const
|
||||
const PrintRegionConfig ®ion_config = region.config();
|
||||
const PrintObject &print_object = *this->layer()->object();
|
||||
Flow bridge_flow;
|
||||
// Here this->extruder(role) - 1 may underflow to MAX_INT, but then the get_at() will fall back to zero'th element, so everything is all right.
|
||||
auto nozzle_diameter = float(print_object.print()->config().nozzle_diameter.get_at(region.extruder(role) - 1));
|
||||
const ConfigOptionFloatOrPercent& bridge_width_opt = region_config.bridge_line_width;
|
||||
const double bridge_width = bridge_width_opt.get_abs_value(nozzle_diameter);
|
||||
const bool has_bridge_width = bridge_width > 0.;
|
||||
const double bridge_flow_ratio = region_config.bridge_flow;
|
||||
|
||||
if (thick_bridge) {
|
||||
// The old Slic3r way (different from all other slicers): Use rounded extrusions.
|
||||
// Get the configured nozzle_diameter for the extruder associated to the flow role requested.
|
||||
// Here this->extruder(role) - 1 may underflow to MAX_INT, but then the get_at() will follback to zero'th element, so everything is all right.
|
||||
// Applies default bridge spacing.
|
||||
bridge_flow = Flow::bridging_flow(float(sqrt(region_config.bridge_flow)) * nozzle_diameter, nozzle_diameter);
|
||||
float thread_diameter = has_bridge_width ? float(bridge_width) : nozzle_diameter;
|
||||
if (bridge_flow_ratio > 0.)
|
||||
thread_diameter *= float(sqrt(bridge_flow_ratio));
|
||||
bridge_flow = Flow::bridging_flow(thread_diameter, nozzle_diameter);
|
||||
} else {
|
||||
// The same way as other slicers: Use normal extrusions. Apply bridge_flow while maintaining the original spacing.
|
||||
bridge_flow = this->flow(role).with_flow_ratio(region_config.bridge_flow);
|
||||
Flow base_flow = this->flow(role);
|
||||
if (has_bridge_width)
|
||||
base_flow = Flow(float(bridge_width), base_flow.height(), nozzle_diameter);
|
||||
bridge_flow = base_flow.with_flow_ratio(bridge_flow_ratio);
|
||||
}
|
||||
return bridge_flow;
|
||||
|
||||
@@ -83,6 +93,12 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
|
||||
(this->layer()->id() >= size_t(region_config.bottom_shell_layers.value) &&
|
||||
this->layer()->print_z >= region_config.bottom_shell_thickness - EPSILON);
|
||||
|
||||
double model_rotation_rad = 0.0;
|
||||
if (region_config.align_infill_direction_to_model) {
|
||||
auto m = this->layer()->object()->trafo().matrix();
|
||||
model_rotation_rad = std::atan2((double)m(1, 0), (double)m(0, 0));
|
||||
}
|
||||
|
||||
PerimeterGenerator g(
|
||||
// input:
|
||||
&slices,
|
||||
@@ -94,6 +110,7 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, const LayerRe
|
||||
&this->layer()->object()->config(),
|
||||
&print_config,
|
||||
spiral_mode,
|
||||
model_rotation_rad,
|
||||
|
||||
// output:
|
||||
&this->perimeters,
|
||||
@@ -517,10 +534,27 @@ void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Poly
|
||||
SurfaceCollection bridges;
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << "Processing external surface, detecting bridges. layer" << this->layer()->print_z;
|
||||
const double custom_angle = this->region().config().bridge_angle.value;
|
||||
bridges.surfaces = custom_angle > 0 ?
|
||||
expand_merge_surfaces(this->fill_surfaces.surfaces, stBottomBridge, expansion_zones, closing_radius, Geometry::deg2rad(custom_angle)) :
|
||||
// ORCA: Relative/Align Bridge Angle
|
||||
const auto ®ion_config = this->region().config();
|
||||
const double custom_angle_deg = region_config.bridge_angle.value;
|
||||
const bool relative_angle = region_config.relative_bridge_angle.value;
|
||||
const double custom_angle_rad = Geometry::deg2rad(custom_angle_deg);
|
||||
|
||||
double align_offset_rad = 0.0;
|
||||
if (region_config.align_infill_direction_to_model) {
|
||||
auto m = this->layer()->object()->trafo().matrix();
|
||||
align_offset_rad = std::atan2((double)m(1, 0), (double)m(0, 0));
|
||||
}
|
||||
|
||||
bridges.surfaces = (custom_angle_deg > 0.0 && !relative_angle) ?
|
||||
expand_merge_surfaces(this->fill_surfaces.surfaces, stBottomBridge, expansion_zones, closing_radius, custom_angle_rad + align_offset_rad) :
|
||||
expand_bridges_detect_orientations(this->fill_surfaces.surfaces, expansion_zones, closing_radius);
|
||||
if (custom_angle_deg > 0.0 && relative_angle) {
|
||||
for (Surface &bridge_surface : bridges.surfaces) {
|
||||
if (bridge_surface.bridge_angle >= 0)
|
||||
bridge_surface.bridge_angle += custom_angle_rad;
|
||||
}
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(trace) << "Processing external surface, detecting bridges - done";
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
@@ -782,12 +816,25 @@ void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Poly
|
||||
// would get merged into a single one while they need different directions
|
||||
// also, supply the original expolygon instead of the grown one, because in case
|
||||
// of very thin (but still working) anchors, the grown expolygon would go beyond them
|
||||
double custom_angle = Geometry::deg2rad(this->region().config().bridge_angle.value);
|
||||
if (custom_angle > 0.0) {
|
||||
bridges[idx_last].bridge_angle = custom_angle;
|
||||
// ORCA: Relative/Align Bridge Angle
|
||||
const auto ®ion_config = this->region().config();
|
||||
const double custom_angle_deg = region_config.bridge_angle.value;
|
||||
const bool relative_angle = region_config.relative_bridge_angle.value;
|
||||
const double custom_angle_rad = Geometry::deg2rad(custom_angle_deg);
|
||||
|
||||
double align_offset_rad = 0.0;
|
||||
if (region_config.align_infill_direction_to_model) {
|
||||
auto m = this->layer()->object()->trafo().matrix();
|
||||
align_offset_rad = std::atan2((double)m(1, 0), (double)m(0, 0));
|
||||
}
|
||||
|
||||
if (custom_angle_deg > 0.0 && !relative_angle) {
|
||||
bridges[idx_last].bridge_angle = custom_angle_rad + align_offset_rad;
|
||||
} else {
|
||||
auto [bridging_dir, unsupported_dist] = detect_bridging_direction(to_polygons(initial), to_polygons(lower_layer->lslices));
|
||||
bridges[idx_last].bridge_angle = PI + std::atan2(bridging_dir.y(), bridging_dir.x());
|
||||
if (custom_angle_deg > 0.0 && relative_angle)
|
||||
bridges[idx_last].bridge_angle += custom_angle_rad;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1760,8 +1760,14 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
|
||||
BridgeDetector detector{ unsupported,
|
||||
lower_island.expolygons,
|
||||
perimeter_spacing / 4}; // Use a finer BridgeDetector. This affects coverage resolution, not extrusion spacing.
|
||||
|
||||
if (detector.detect_angle(Geometry::deg2rad(this->config->bridge_angle.value)))
|
||||
// ORCA: Relative/Align Bridge Angle
|
||||
const double custom_angle_deg = this->config->bridge_angle.value;
|
||||
const bool relative_angle = this->config->relative_bridge_angle.value;
|
||||
const double detect_angle_rad = (custom_angle_deg > 0.0 && !relative_angle)
|
||||
? Geometry::deg2rad(custom_angle_deg) +
|
||||
(this->config->align_infill_direction_to_model ? this->m_model_rotation_rad : 0.0)
|
||||
: 0.0;
|
||||
if (detector.detect_angle(detect_angle_rad))
|
||||
expolygons_append(bridgeable, union_ex(detector.coverage(-1, true)));
|
||||
}
|
||||
if (!bridgeable.empty() && !surface->expolygon.holes.empty()) { // keep out if cannot be bridged or no holes to bridge
|
||||
|
||||
@@ -117,6 +117,7 @@ public:
|
||||
const PrintObjectConfig* object_config,
|
||||
const PrintConfig* print_config,
|
||||
const bool spiral_mode,
|
||||
const double model_rotation_rad,
|
||||
// Output:
|
||||
// Loops with the external thin walls
|
||||
ExtrusionEntityCollection* loops,
|
||||
@@ -132,6 +133,7 @@ public:
|
||||
config(config), object_config(object_config), print_config(print_config),
|
||||
m_spiral_vase(spiral_mode),
|
||||
m_scaled_resolution(scaled<double>(print_config->resolution.value > EPSILON ? print_config->resolution.value : EPSILON)),
|
||||
m_model_rotation_rad(model_rotation_rad),
|
||||
loops(loops), gap_fill(gap_fill), fill_surfaces(fill_surfaces), fill_no_overlap(fill_no_overlap),
|
||||
m_ext_mm3_per_mm(-1), m_mm3_per_mm(-1), m_mm3_per_mm_overhang(-1), m_ext_mm3_per_mm_smaller_width(-1)
|
||||
{}
|
||||
@@ -157,6 +159,7 @@ private:
|
||||
private:
|
||||
bool m_spiral_vase;
|
||||
double m_scaled_resolution;
|
||||
double m_model_rotation_rad;
|
||||
double m_ext_mm3_per_mm;
|
||||
double m_mm3_per_mm;
|
||||
double m_mm3_per_mm_overhang;
|
||||
|
||||
@@ -1097,6 +1097,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"infill_wall_overlap",
|
||||
"top_bottom_infill_wall_overlap",
|
||||
"bridge_flow",
|
||||
"bridge_line_width",
|
||||
"internal_bridge_flow",
|
||||
"elefant_foot_compensation",
|
||||
"elefant_foot_compensation_layers",
|
||||
@@ -1161,6 +1162,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"small_perimeter_threshold",
|
||||
"bridge_angle",
|
||||
"internal_bridge_angle",
|
||||
"relative_bridge_angle",
|
||||
"filter_out_gap_fill",
|
||||
"travel_acceleration",
|
||||
"inner_wall_acceleration",
|
||||
|
||||
@@ -1540,12 +1540,12 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
|
||||
auto validate_extrusion_width = [min_nozzle_diameter, max_nozzle_diameter](const ConfigBase &config, const char *opt_key, double layer_height, std::string &err_msg) -> bool {
|
||||
double extrusion_width_min = config.get_abs_value(opt_key, min_nozzle_diameter);
|
||||
double extrusion_width_max = config.get_abs_value(opt_key, max_nozzle_diameter);
|
||||
if (extrusion_width_min == 0) {
|
||||
// Default "auto-generated" extrusion width is always valid.
|
||||
} else if (extrusion_width_min <= layer_height) {
|
||||
err_msg = L("Too small line width");
|
||||
return false;
|
||||
} else if (extrusion_width_max > max_nozzle_diameter * MAX_LINE_WIDTH_MULTIPLIER) {
|
||||
if (extrusion_width_min == 0) {
|
||||
// Default "auto-generated" extrusion width is always valid.
|
||||
} else if (extrusion_width_min <= layer_height) {
|
||||
err_msg = L("Too small line width");
|
||||
return false;
|
||||
} else if (extrusion_width_max > max_nozzle_diameter * MAX_LINE_WIDTH_MULTIPLIER) {
|
||||
err_msg = L("Too large line width");
|
||||
return false;
|
||||
}
|
||||
@@ -1667,6 +1667,25 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
|
||||
for (const PrintRegion ®ion : object->all_regions())
|
||||
if (!validate_extrusion_width(region.config(), opt_key, layer_height, err_msg))
|
||||
return {err_msg, object, opt_key};
|
||||
|
||||
const bool allow_thin_bridge_width = object->config().thick_bridges && object->config().thick_internal_bridges;
|
||||
for (const PrintRegion ®ion : object->all_regions()) {
|
||||
const auto &bridge_width_opt = region.config().bridge_line_width;
|
||||
for (FlowRole bridge_role : { frPerimeter, frInfill, frSolidInfill, frTopSolidInfill }) {
|
||||
const double nozzle_diameter = m_config.nozzle_diameter.get_at(region.extruder(bridge_role) - 1);
|
||||
const double bridge_width = bridge_width_opt.get_abs_value(nozzle_diameter);
|
||||
if (bridge_width <= 0.)
|
||||
continue;
|
||||
if (bridge_width > nozzle_diameter) {
|
||||
err_msg = L("Bridge line width must not exceed nozzle diameter");
|
||||
return { err_msg, object, "bridge_line_width" };
|
||||
}
|
||||
if (!allow_thin_bridge_width && bridge_width <= layer_height) {
|
||||
err_msg = L("Too small line width");
|
||||
return { err_msg, object, "bridge_line_width" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,8 @@ static t_config_enum_values s_keys_map_PrintHostType {
|
||||
{ "obico", htObico },
|
||||
{ "flashforge", htFlashforge },
|
||||
{ "simplyprint", htSimplyPrint },
|
||||
{ "elegoolink", htElegooLink }
|
||||
{ "elegoolink", htElegooLink },
|
||||
{ "3dprinteros", ht3DPrinterOS }
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType)
|
||||
|
||||
@@ -1241,11 +1242,16 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("External bridge infill direction");
|
||||
def->category = L("Strength");
|
||||
// xgettext:no-c-format, no-boost-format
|
||||
def->tooltip = L("Bridging angle override. If left to zero, the bridging angle will be calculated "
|
||||
"automatically. Otherwise the provided angle will be used for external bridges. "
|
||||
"Use 180° for zero angle.");
|
||||
def->tooltip = L("External Bridging angle override.\n"
|
||||
"If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n"
|
||||
"Otherwise the provided angle will be used according to:\n"
|
||||
" - The absolute coordinates\n"
|
||||
" - The absolute coordinates + Model rotation: If Align infill direction to model is enabled\n"
|
||||
" - The optimal automatic angle + this value: If 'Relative Bridge Angle' is enabled\n\n"
|
||||
"Use 180° for zero absolute angle.");
|
||||
def->sidetext = u8"°"; // degrees, don't need translation
|
||||
def->min = 0;
|
||||
def->max = 180;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(0.));
|
||||
|
||||
@@ -1253,58 +1259,97 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("internal_bridge_angle", coFloat);
|
||||
def->label = L("Internal bridge infill direction");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Internal bridging angle override. If left to zero, the bridging angle will be calculated "
|
||||
"automatically. Otherwise the provided angle will be used for internal bridges. "
|
||||
"Use 180° for zero angle.\n\nIt is recommended to leave it at 0 unless there is a specific model need not to.");
|
||||
def->tooltip = L("Internal Bridging angle override.\n"
|
||||
"If left to zero, the bridging angle will be calculated automatically for each specific bridge.\n"
|
||||
"Otherwise the provided angle will be used according to:\n"
|
||||
" - The absolute coordinates\n"
|
||||
" - The absolute coordinates + Model rotation: If Align infill direction to model is enabled\n"
|
||||
" - The optimal automatic angle + this value: If 'Relative Bridge Angle' is enabled\n\n"
|
||||
"Use 180° for zero absolute angle.");
|
||||
def->sidetext = u8"°"; // degrees, don't need translation
|
||||
def->min = 0;
|
||||
def->max = 180;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(0.));
|
||||
|
||||
// ORCA: Relative bridge angle
|
||||
def = this->add("relative_bridge_angle", coBool);
|
||||
def->label = L("Relative bridge angle");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("When enabled, the bridge angle values are added to the automatically calculated bridge direction instead of overriding it.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("bridge_density", coPercent);
|
||||
def->label = L("External bridge density");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Controls the density (spacing) of external bridge lines. Default is 100%.\n\n"
|
||||
"Lower density external bridges can help improve reliability as there is more space for air to circulate "
|
||||
"around the extruded bridge, improving its cooling speed. Minimum is 10%.\n\n"
|
||||
"Higher densities can produce smoother bridge surfaces, as overlapping lines provide "
|
||||
"additional support during printing. Maximum is 120%.\n"
|
||||
"Note: Bridge density that is too high can cause warping or overextrusion.");
|
||||
def->tooltip = L("Controls the density (spacing) of external bridge lines. Default is 100%.\n"
|
||||
"Theoretically, 100% means a solid bridge, but due to the tendency of bridge extrusions to sag, 100% may not be sufficient.\n\n"
|
||||
"- Higher than 100% density (Recommended Max 125%):\n"
|
||||
" - Pros: Produces smoother bridge surfaces, as overlapping lines provide additional support during printing.\n"
|
||||
" - Cons: Can cause overextrusion, which may reduce lower and upper surface quality and increase the risk of warping.\n\n"
|
||||
"- Lower than 100% density (Min 10%):\n"
|
||||
" - Pros: Can create a string-like first layer. Faster and with better cooling because there is more space for air to circulate around the extruded bridge.\n"
|
||||
" - Cons: May lead to sagging and poorer surface finish.\n\n"
|
||||
"Recommended range: Minimum 10% - Maximum 125%.");
|
||||
def->sidetext = "%";
|
||||
def->min = 10;
|
||||
def->max = 120;
|
||||
def->max = 125;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionPercent(100));
|
||||
|
||||
def = this->add("internal_bridge_density", coPercent);
|
||||
def->label = L("Internal bridge density");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Controls the density (spacing) of internal bridge lines. 100% means solid bridge. Default is 100%.\n\n"
|
||||
"Lower density internal bridges can help reduce top surface pillowing and improve internal bridge reliability as there is more space for "
|
||||
"air to circulate around the extruded bridge, improving its cooling speed.\n\n"
|
||||
"This option works particularly well when combined with the second internal bridge over infill option, "
|
||||
"further improving internal bridging structure before solid infill is extruded.");
|
||||
def->tooltip = L("Controls the density (spacing) of internal bridge lines. Default is 100%. 100% means a solid internal bridge.\n\n"
|
||||
"Internal bridges act as intermediate support between sparse infill and top solid infill and can strongly affect top surface quality.\n\n"
|
||||
"- Higher than 100% density (Recommended Max 125%):\n"
|
||||
" - Pros: Improves internal bridge strength and support under top layers, reducing sagging and improving top-surface finish.\n"
|
||||
" - Cons: Increases material use and print time; excessive density may cause overextrusion and internal stresses.\n\n"
|
||||
"- Lower than 100% density (Min 10%):\n"
|
||||
" - Pros: Can reduce pillowing and improve cooling (more airflow through the bridge), and may speed up printing.\n"
|
||||
" - Cons: May reduce internal support, increasing the risk of sagging and top surface defects.\n\n"
|
||||
"This option works particularly well when combined with the second internal bridge over infill option to improve bridging further before solid infill is extruded.");
|
||||
def->sidetext = "%";
|
||||
def->min = 10;
|
||||
def->max = 100;
|
||||
def->max = 125;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionPercent(100));
|
||||
|
||||
def = this->add("bridge_flow", coFloat);
|
||||
def->label = L("Bridge flow ratio");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("Decrease this value slightly (for example 0.9) to reduce the amount of material for bridge, to improve sag.\n\n"
|
||||
def->tooltip = L("This value governs the thickness of the external (visible) bridge layer.\n"
|
||||
"Values above 1.0: Increase the amount of material while maintaining line spacing. This can improve line contact and strength.\n"
|
||||
"Values below 1.0: Reduce the amount of material while adjusting line spacing to maintain contact. This can improve sagging.\n\n"
|
||||
"The actual bridge flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio.");
|
||||
def->min = 0;
|
||||
def->max = 2.0;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(1));
|
||||
|
||||
def = this->add("bridge_line_width", coFloatOrPercent);
|
||||
def->label = L("Bridge");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("Bridge line width is expressed either as an absolute value or as a percentage of the active nozzle diameter (percentages are computed from the nozzle diameter).\n"
|
||||
"Recommended to use with a higher Bridge density or Bridge flow ratio.\n\n"
|
||||
"The maximum value is 100% or the nozzle diameter.\n"
|
||||
"If set to 0, the line width will match the Internal solid infill width.");
|
||||
def->sidetext = L("mm or %");
|
||||
def->ratio_over = "nozzle_diameter";
|
||||
def->min = 0;
|
||||
def->max = 100;
|
||||
def->max_literal = 10;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloatOrPercent(100., true));
|
||||
|
||||
def = this->add("internal_bridge_flow", coFloat);
|
||||
def->label = L("Internal bridge flow ratio");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill. Decrease this value slightly (for example 0.9) to improve surface quality over sparse infill."
|
||||
"\n\nThe actual internal bridge flow used is calculated by multiplying this value with the bridge flow ratio, the filament flow ratio, and if set, the object's flow ratio.");
|
||||
def->tooltip = L("This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill so increasing it may increase strength and upper layer quality.\n"
|
||||
"Values above 1.0: Increase the amount of material while maintaining line spacing. This can improve line contact and strength.\n"
|
||||
"Values below 1.0: Reduce the amount of material while adjusting line spacing to maintain contact. This can improve sagging.\n\n"
|
||||
"The actual bridge flow used is calculated by multiplying this value with the filament flow ratio, and if set, the object's flow ratio.");
|
||||
def->min = 0;
|
||||
def->max = 2.0;
|
||||
def->mode = comAdvanced;
|
||||
@@ -1888,16 +1933,18 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("thick_bridges", coBool);
|
||||
def->label = L("Thick external bridges");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("If enabled, bridges are more reliable, can bridge longer distances, but may look worse. "
|
||||
"If disabled, bridges look better but are reliable just for shorter bridged distances.");
|
||||
def->tooltip = L("If enabled, bridge extrusion uses a line height equal to the nozzle diameter.\n"
|
||||
"This increases bridge strength and reliability, allowing longer spans, but may worsen appearance.\n"
|
||||
"If disabled, bridges may look better but are generally reliable only for shorter spans.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("thick_internal_bridges", coBool);
|
||||
def->label = L("Thick internal bridges");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("If enabled, thick internal bridges will be used. It's usually recommended to have this feature turned on. However, "
|
||||
"consider turning it off if you are using large nozzles.");
|
||||
def->tooltip = L("If enabled, internal bridge extrusion uses a line height equal to the nozzle diameter.\n"
|
||||
"This increases internal bridge strength and reliability when printed over sparse infill, but may worsen appearance.\n"
|
||||
"If disabled, internal bridges may look better but can be less reliable over sparse infill.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(true));
|
||||
|
||||
@@ -2924,7 +2971,8 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("align_infill_direction_to_model", coBool);
|
||||
def->label = L("Align infill direction to model");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Aligns infill and surface fill directions to follow the model's orientation on the build plate. When enabled, fill directions rotate with the model to maintain optimal strength characteristics.");
|
||||
def->tooltip = L("Aligns infill, bridge, ironing and surface fill directions to follow the model's orientation on the build plate.\n"
|
||||
"When enabled, directions rotate with the model to maintain optimal strength characteristics.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
@@ -4813,6 +4861,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_values.push_back("flashforge");
|
||||
def->enum_values.push_back("simplyprint");
|
||||
def->enum_values.push_back("elegoolink");
|
||||
def->enum_values.push_back("3dprinteros");
|
||||
def->enum_labels.push_back("PrusaLink");
|
||||
def->enum_labels.push_back("PrusaConnect");
|
||||
def->enum_labels.push_back("Octo/Klipper");
|
||||
@@ -4827,6 +4876,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back("Flashforge");
|
||||
def->enum_labels.push_back("SimplyPrint");
|
||||
def->enum_labels.push_back("Elegoo Link");
|
||||
def->enum_labels.push_back("3DPrinterOS");
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionEnum<PrintHostType>(htOctoPrint));
|
||||
@@ -9700,6 +9750,7 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (optdef->type) {
|
||||
case coStrings:
|
||||
{
|
||||
@@ -10249,8 +10300,8 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
error_message.emplace("bridge_flow", L("invalid value ") + std::to_string(cfg.bridge_flow));
|
||||
}
|
||||
|
||||
// --bridge-flow-ratio
|
||||
if (cfg.bridge_flow <= 0) {
|
||||
// --internal-bridge-flow-ratio
|
||||
if (cfg.internal_bridge_flow <= 0) {
|
||||
error_message.emplace("internal_bridge_flow", L("invalid value ") + std::to_string(cfg.internal_bridge_flow));
|
||||
}
|
||||
|
||||
@@ -10308,13 +10359,18 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
// extrusion widths
|
||||
{
|
||||
double max_nozzle_diameter = 0.;
|
||||
double min_nozzle_diameter = std::numeric_limits<double>::max();
|
||||
for (double dmr : cfg.nozzle_diameter.values)
|
||||
{
|
||||
max_nozzle_diameter = std::max(max_nozzle_diameter, dmr);
|
||||
min_nozzle_diameter = std::min(min_nozzle_diameter, dmr);
|
||||
}
|
||||
const char *widths[] = {
|
||||
"outer_wall_line_width",
|
||||
"inner_wall_line_width",
|
||||
"sparse_infill_line_width",
|
||||
"internal_solid_infill_line_width",
|
||||
"bridge_line_width",
|
||||
"top_surface_line_width",
|
||||
"support_line_width",
|
||||
"initial_layer_line_width",
|
||||
@@ -10322,8 +10378,13 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
|
||||
"skeleton_infill_line_width"};
|
||||
for (size_t i = 0; i < sizeof(widths) / sizeof(widths[i]); ++ i) {
|
||||
std::string key(widths[i]);
|
||||
if (cfg.get_abs_value(key, max_nozzle_diameter) > MAX_LINE_WIDTH_MULTIPLIER * max_nozzle_diameter) {
|
||||
error_message.emplace(key, L("too large line width ") + std::to_string(cfg.get_abs_value(key)));
|
||||
double abs_width = cfg.get_abs_value(key, max_nozzle_diameter);
|
||||
double allowed_max = (key == "bridge_line_width") ? min_nozzle_diameter : MAX_LINE_WIDTH_MULTIPLIER * max_nozzle_diameter;
|
||||
if (abs_width > allowed_max) {
|
||||
if (key == "bridge_line_width")
|
||||
error_message.emplace(key, L("Bridge line width must not exceed nozzle diameter: ") + std::to_string(abs_width));
|
||||
else
|
||||
error_message.emplace(key, L("too large line width ") + std::to_string(abs_width));
|
||||
//return std::string("Too Large line width: ") + key;
|
||||
}
|
||||
}
|
||||
@@ -10786,24 +10847,17 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def->cli_params = "level";
|
||||
def->set_default_value(new ConfigOptionInt(1));
|
||||
|
||||
def = this->add("logfile", coInt);
|
||||
def->label = L("Log file");
|
||||
def->tooltip = L("Redirects debug logging to file.\n");
|
||||
def->cli_params = "file";
|
||||
def->set_default_value(new ConfigOptionString());
|
||||
|
||||
def = this->add("enable_timelapse", coBool);
|
||||
def->label = L("Enable timelapse for print");
|
||||
def->tooltip = L("If enabled, this slicing will be considered using timelapse.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("automation_server", coBool);
|
||||
def->label = L("Enable UI automation server");
|
||||
def->tooltip = L("Start a localhost JSON-RPC server that lets external scripts "
|
||||
"drive and observe the GUI. For testing/automation only.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("automation_server_port", coInt);
|
||||
def->label = L("UI automation server port");
|
||||
def->tooltip = L("TCP port for the UI automation server (bound to 127.0.0.1).");
|
||||
def->min = 1;
|
||||
def->cli_params = "port";
|
||||
def->set_default_value(new ConfigOptionInt(13619));
|
||||
|
||||
#if (defined(_MSC_VER) || defined(__MINGW32__)) && defined(SLIC3R_GUI)
|
||||
/*def = this->add("sw_renderer", coBool);
|
||||
def->label = L("Render with a software renderer");
|
||||
|
||||
@@ -77,7 +77,7 @@ enum class WipeTowerType {
|
||||
};
|
||||
|
||||
enum PrintHostType {
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS
|
||||
};
|
||||
|
||||
enum AuthorizationType {
|
||||
@@ -1080,7 +1080,9 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionFloat, bottom_shell_thickness))
|
||||
((ConfigOptionFloat, bridge_angle))
|
||||
((ConfigOptionFloat, internal_bridge_angle)) // ORCA: Internal bridge angle override
|
||||
((ConfigOptionBool, relative_bridge_angle)) // ORCA: Relative bridge angle flag
|
||||
((ConfigOptionFloat, bridge_flow))
|
||||
((ConfigOptionFloatOrPercent, bridge_line_width))
|
||||
((ConfigOptionFloat, internal_bridge_flow))
|
||||
((ConfigOptionFloat, bridge_speed))
|
||||
((ConfigOptionFloatOrPercent, internal_bridge_speed))
|
||||
|
||||
@@ -1275,7 +1275,9 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "ensure_vertical_shell_thickness"
|
||||
|| opt_key == "bridge_angle"
|
||||
|| opt_key == "internal_bridge_angle" // ORCA: Internal bridge angle override
|
||||
|| opt_key == "relative_bridge_angle" // ORCA: Relative bridge angle
|
||||
//BBS
|
||||
|| opt_key == "bridge_line_width"
|
||||
|| opt_key == "bridge_density"
|
||||
|| opt_key == "internal_bridge_density") {
|
||||
steps.emplace_back(posPrepareInfill);
|
||||
@@ -3217,8 +3219,19 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
|
||||
// ORCA: Internal bridge angle override
|
||||
if (candidate.region->region().config().internal_bridge_angle > 0)
|
||||
bridging_angle = candidate.region->region().config().internal_bridge_angle.value * PI / 180.0; // Convert degrees to radians
|
||||
if (candidate.region->region().config().internal_bridge_angle.value > 0) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const double custom_angle_rad = Geometry::deg2rad(region_config.internal_bridge_angle.value);
|
||||
if (region_config.relative_bridge_angle.value)
|
||||
bridging_angle += custom_angle_rad;
|
||||
else {
|
||||
bridging_angle = custom_angle_rad;
|
||||
if (region_config.align_infill_direction_to_model) {
|
||||
auto m = po->trafo().matrix();
|
||||
bridging_angle += std::atan2((double)m(1, 0), (double)m(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boundary_plines.insert(boundary_plines.end(), anchors.begin(), anchors.end());
|
||||
if (!lightning_area.empty() && !intersection(area_to_be_bridge, lightning_area).empty()) {
|
||||
|
||||
@@ -79,6 +79,7 @@ namespace boost { namespace filesystem { class directory_entry; }}
|
||||
namespace Slic3r {
|
||||
|
||||
extern void set_logging_level(unsigned int level);
|
||||
extern void set_logging_file(const std::string &file);
|
||||
extern unsigned int level_string_to_boost(std::string level);
|
||||
extern std::string get_string_logging_level(unsigned level);
|
||||
extern unsigned get_logging_level();
|
||||
|
||||
@@ -129,6 +129,11 @@ void set_logging_level(unsigned int level)
|
||||
);
|
||||
}
|
||||
|
||||
void set_logging_file(const std::string &file)
|
||||
{
|
||||
boost::log::add_file_log(file);
|
||||
}
|
||||
|
||||
unsigned int level_string_to_boost(std::string level)
|
||||
{
|
||||
std::map<std::string, int> Control_Param;
|
||||
|
||||
@@ -230,21 +230,6 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/HMSPanel.hpp
|
||||
GUI/HttpServer.cpp
|
||||
GUI/HttpServer.hpp
|
||||
GUI/Automation/IUiBackend.hpp
|
||||
GUI/Automation/WidgetSerializer.cpp
|
||||
GUI/Automation/WidgetSerializer.hpp
|
||||
GUI/Automation/Locator.cpp
|
||||
GUI/Automation/Locator.hpp
|
||||
GUI/Automation/JsonRpcDispatcher.cpp
|
||||
GUI/Automation/JsonRpcDispatcher.hpp
|
||||
GUI/Automation/AutomationServer.cpp
|
||||
GUI/Automation/AutomationServer.hpp
|
||||
GUI/Automation/AutomationRegistry.cpp
|
||||
GUI/Automation/AutomationRegistry.hpp
|
||||
GUI/Automation/ImGuiItemTable.cpp
|
||||
GUI/Automation/ImGuiItemTable.hpp
|
||||
GUI/Automation/WxUiBackend.cpp
|
||||
GUI/Automation/WxUiBackend.hpp
|
||||
GUI/I18N.cpp
|
||||
GUI/I18N.hpp
|
||||
GUI/DragDropPanel.cpp
|
||||
@@ -674,6 +659,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/UndoRedo.cpp
|
||||
Utils/UndoRedo.hpp
|
||||
Utils/WebSocketClient.hpp
|
||||
Utils/3DPrinterOS.hpp
|
||||
Utils/3DPrinterOS.cpp
|
||||
Utils/WxFontUtils.cpp
|
||||
Utils/WxFontUtils.hpp
|
||||
Utils/FileTransferUtils.cpp
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#include "AutomationRegistry.hpp"
|
||||
#include <wx/window.h>
|
||||
#include <wx/event.h>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
namespace {
|
||||
std::mutex& mtx() { static std::mutex m; return m; }
|
||||
std::unordered_map<const wxWindow*, std::string>& fwd() {
|
||||
static std::unordered_map<const wxWindow*, std::string> m; return m;
|
||||
}
|
||||
std::unordered_map<std::string, wxWindow*>& rev() {
|
||||
static std::unordered_map<std::string, wxWindow*> m; return m;
|
||||
}
|
||||
void erase_window(const wxWindow* w) {
|
||||
std::lock_guard<std::mutex> lk(mtx());
|
||||
auto it = fwd().find(w);
|
||||
if (it != fwd().end()) { rev().erase(it->second); fwd().erase(it); }
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void set_automation_id(wxWindow* window, const std::string& id) {
|
||||
if (window == nullptr || id.empty()) return;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mtx());
|
||||
fwd()[window] = id;
|
||||
rev()[id] = window;
|
||||
}
|
||||
// Prune on destruction.
|
||||
window->Bind(wxEVT_DESTROY, [window](wxWindowDestroyEvent& e) {
|
||||
erase_window(window);
|
||||
e.Skip();
|
||||
});
|
||||
}
|
||||
|
||||
std::string automation_id_of(const wxWindow* window) {
|
||||
std::lock_guard<std::mutex> lk(mtx());
|
||||
auto it = fwd().find(window);
|
||||
return it == fwd().end() ? std::string() : it->second;
|
||||
}
|
||||
|
||||
wxWindow* window_for_automation_id(const std::string& id) {
|
||||
std::lock_guard<std::mutex> lk(mtx());
|
||||
auto it = rev().find(id);
|
||||
return it == rev().end() ? nullptr : it->second;
|
||||
}
|
||||
|
||||
}}} // namespace
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
class wxWindow;
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
// Process-wide wxWindow* <-> automation_id side map. Header is dependency-light so
|
||||
// widget-construction code can call set_automation_id() unconditionally — it is a
|
||||
// cheap, safe registration that no-ops when the window is null.
|
||||
//
|
||||
// Registration is pruned automatically when the window is destroyed (bound to
|
||||
// wxEVT_DESTROY inside set_automation_id).
|
||||
void set_automation_id(wxWindow* window, const std::string& id);
|
||||
std::string automation_id_of(const wxWindow* window); // "" if none
|
||||
wxWindow* window_for_automation_id(const std::string& id); // nullptr if none
|
||||
|
||||
}}} // namespace
|
||||
@@ -1,100 +0,0 @@
|
||||
#include "AutomationServer.hpp"
|
||||
#include "libslic3r/Thread.hpp" // create_thread / set_current_thread_name
|
||||
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace net = boost::asio;
|
||||
using tcp = net::ip::tcp;
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
AutomationServer::AutomationServer(unsigned short port) : m_port(port) {}
|
||||
|
||||
AutomationServer::~AutomationServer() { stop(); }
|
||||
|
||||
void AutomationServer::start() {
|
||||
if (m_started) return;
|
||||
m_ioc = std::make_unique<net::io_context>(1);
|
||||
// Bind to loopback ONLY.
|
||||
tcp::endpoint endpoint(net::ip::make_address("127.0.0.1"), m_port);
|
||||
m_acceptor = std::make_unique<tcp::acceptor>(*m_ioc);
|
||||
m_acceptor->open(endpoint.protocol());
|
||||
m_acceptor->set_option(net::socket_base::reuse_address(true));
|
||||
m_acceptor->bind(endpoint);
|
||||
m_acceptor->listen(net::socket_base::max_listen_connections);
|
||||
m_started = true;
|
||||
|
||||
do_accept();
|
||||
|
||||
net::io_context* ioc = m_ioc.get();
|
||||
m_thread = create_thread([ioc] {
|
||||
set_current_thread_name("orca_automation");
|
||||
ioc->run();
|
||||
});
|
||||
BOOST_LOG_TRIVIAL(info) << "AutomationServer listening on 127.0.0.1:" << m_port;
|
||||
}
|
||||
|
||||
void AutomationServer::stop() {
|
||||
if (!m_started) return;
|
||||
m_started = false;
|
||||
if (m_ioc) m_ioc->stop();
|
||||
if (m_thread.joinable()) m_thread.join();
|
||||
m_acceptor.reset();
|
||||
m_ioc.reset();
|
||||
}
|
||||
|
||||
void AutomationServer::do_accept() {
|
||||
m_acceptor->async_accept([this](beast::error_code ec, tcp::socket socket) {
|
||||
if (!ec) {
|
||||
// v1: single-client, serialized — handle synchronously on the io thread.
|
||||
handle_session(std::move(socket));
|
||||
}
|
||||
if (m_started && m_acceptor && m_acceptor->is_open())
|
||||
do_accept();
|
||||
});
|
||||
}
|
||||
|
||||
void AutomationServer::handle_session(tcp::socket socket) {
|
||||
beast::error_code ec;
|
||||
beast::flat_buffer buffer;
|
||||
http::request<http::string_body> req;
|
||||
http::read(socket, buffer, req, ec);
|
||||
if (ec) { socket.shutdown(tcp::socket::shutdown_send, ec); return; }
|
||||
|
||||
http::response<http::string_body> res;
|
||||
res.version(req.version());
|
||||
res.keep_alive(false);
|
||||
|
||||
if (req.method() == http::verb::post && req.target() == "/jsonrpc") {
|
||||
std::string body_out;
|
||||
try {
|
||||
body_out = m_handler ? m_handler(req.body())
|
||||
: R"({"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"no handler"}})";
|
||||
} catch (const std::exception& e) {
|
||||
body_out = std::string(R"({"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":")")
|
||||
+ e.what() + R"("}})";
|
||||
}
|
||||
res.result(http::status::ok);
|
||||
res.set(http::field::content_type, "application/json");
|
||||
res.body() = std::move(body_out);
|
||||
} else if (req.method() == http::verb::get && req.target() == "/") {
|
||||
res.result(http::status::ok);
|
||||
res.set(http::field::content_type, "text/plain");
|
||||
res.body() = m_health;
|
||||
} else {
|
||||
res.result(http::status::not_found);
|
||||
res.set(http::field::content_type, "text/plain");
|
||||
res.body() = "not found";
|
||||
}
|
||||
res.set(http::field::server, "OrcaSlicer/automation");
|
||||
res.prepare_payload();
|
||||
http::write(socket, res, ec);
|
||||
socket.shutdown(tcp::socket::shutdown_send, ec);
|
||||
}
|
||||
|
||||
}}} // namespace
|
||||
@@ -1,42 +0,0 @@
|
||||
#pragma once
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/thread.hpp>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
// Localhost-only HTTP/1.1 server. POST /jsonrpc -> handler(body) -> response body.
|
||||
// GET / -> a tiny health/version page. The handler runs on the server's own
|
||||
// io thread; it is responsible for any further thread marshaling.
|
||||
class AutomationServer {
|
||||
public:
|
||||
using RequestHandler = std::function<std::string(const std::string& body)>;
|
||||
|
||||
explicit AutomationServer(unsigned short port);
|
||||
~AutomationServer();
|
||||
|
||||
void set_handler(RequestHandler handler) { m_handler = std::move(handler); }
|
||||
void set_health_text(std::string text) { m_health = std::move(text); }
|
||||
|
||||
void start(); // binds to 127.0.0.1:port, starts the io thread
|
||||
void stop(); // stops the io thread, joins
|
||||
bool is_started() const { return m_started; }
|
||||
unsigned short port() const { return m_port; }
|
||||
|
||||
private:
|
||||
void do_accept();
|
||||
void handle_session(boost::asio::ip::tcp::socket socket);
|
||||
|
||||
unsigned short m_port;
|
||||
std::atomic<bool> m_started{false};
|
||||
RequestHandler m_handler;
|
||||
std::string m_health{"OrcaSlicer automation server"};
|
||||
std::unique_ptr<boost::asio::io_context> m_ioc;
|
||||
std::unique_ptr<boost::asio::ip::tcp::acceptor> m_acceptor;
|
||||
boost::thread m_thread;
|
||||
};
|
||||
|
||||
}}} // namespace
|
||||
@@ -1,113 +0,0 @@
|
||||
#pragma once
|
||||
// PURE header: no wx / ImGui / GL includes. Safe to compile in the display-free
|
||||
// unit-test target. Shared by the dispatcher, serializer, locator, and backends.
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
enum class BackendKind { Wx, ImGui };
|
||||
|
||||
struct Rect { int x = 0, y = 0, w = 0, h = 0; };
|
||||
|
||||
// One node of the unified UI tree. `handle` is opaque (wxWindow* cast to uintptr_t
|
||||
// for wx, item index for ImGui); it is used by WxUiBackend to recover concrete
|
||||
// objects and is NEVER serialized.
|
||||
struct UiNode {
|
||||
BackendKind backend = BackendKind::Wx;
|
||||
std::string id; // automation id if set, else derived path id
|
||||
std::string path; // positional path, e.g. "MainFrame/Panel[2]/Button[0]"
|
||||
std::string klass; // wx class name or imgui item type
|
||||
std::string label;
|
||||
Rect rect; // screen coordinates
|
||||
bool enabled = true;
|
||||
bool visible = true;
|
||||
bool has_value = false;
|
||||
std::string value; // when applicable (text/choice/check/slider)
|
||||
std::uint64_t handle = 0;
|
||||
std::vector<UiNode> children; // wx only; imgui items are flat under their window
|
||||
};
|
||||
|
||||
struct DumpOptions {
|
||||
std::optional<std::string> root; // id/path to root the dump at
|
||||
int max_depth = -1; // -1 = unlimited
|
||||
bool visible_only = false;
|
||||
bool include_imgui = true;
|
||||
};
|
||||
|
||||
enum class MouseButton { Left, Right, Middle };
|
||||
enum class KeyModifier { Ctrl, Shift, Alt, Cmd };
|
||||
|
||||
struct KeyChord {
|
||||
std::vector<KeyModifier> modifiers;
|
||||
std::string key; // normalized lowercase: "s", "enter", "f5", "tab", ...
|
||||
};
|
||||
|
||||
struct AppState {
|
||||
std::string active_tab;
|
||||
bool project_loaded = false;
|
||||
bool slicing = false;
|
||||
int slice_progress = -1; // -1 = unknown
|
||||
std::optional<std::string> modal_dialog;
|
||||
bool foreground = false;
|
||||
};
|
||||
|
||||
struct PngImage {
|
||||
std::vector<unsigned char> png; // encoded PNG bytes
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
// Thrown by backends/dispatcher; carries a JSON-RPC application error code.
|
||||
struct AutomationError : std::runtime_error {
|
||||
int code;
|
||||
AutomationError(int code, std::string msg)
|
||||
: std::runtime_error(std::move(msg)), code(code) {}
|
||||
};
|
||||
|
||||
// Backend abstraction. The dispatcher orchestrates; the backend only snapshots
|
||||
// and executes primitives on already-resolved nodes.
|
||||
class IUiBackend {
|
||||
public:
|
||||
virtual ~IUiBackend() = default;
|
||||
|
||||
// Force a fresh frame so transient ImGui items are recorded before a read or
|
||||
// action. No-op for non-GUI backends.
|
||||
virtual void refresh_ui() = 0;
|
||||
|
||||
// Snapshot the UI tree (wx hierarchy + flat imgui items under their windows).
|
||||
virtual UiNode dump_tree(const DumpOptions& opts) = 0;
|
||||
|
||||
// Application-level state snapshot.
|
||||
virtual AppState app_state() = 0;
|
||||
|
||||
// Click a resolved node (uses its rect/handle). Raises/focuses first.
|
||||
virtual bool click(const UiNode& node, MouseButton button, bool dbl,
|
||||
const std::vector<KeyModifier>& modifiers) = 0;
|
||||
// Type into the currently-focused control.
|
||||
virtual bool type_text(const std::string& text) = 0;
|
||||
// Send key chords (e.g. ctrl+s) to the focused window.
|
||||
virtual bool send_keys(const std::vector<KeyChord>& chords) = 0;
|
||||
|
||||
// Screenshot. target == nullptr => main frame. Captured from the on-screen
|
||||
// composited framebuffer, so it includes the GL viewport and ImGui overlays.
|
||||
virtual PngImage screenshot_window(const UiNode* target) = 0;
|
||||
|
||||
// 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;
|
||||
|
||||
// Select a top-level view/tab by stable name (e.g. "prepare", "preview", "home",
|
||||
// "device", "project", "calibration", "multi_device") on the GUI thread. Returns
|
||||
// the resulting tab index. Throws AutomationError(kErrNotFound) when the named
|
||||
// view is unknown or not available in the current layout. The wx-specific
|
||||
// name->tab mapping lives in WxUiBackend/MainFrame, not here.
|
||||
virtual int select_view(const std::string& view) = 0;
|
||||
};
|
||||
|
||||
}}} // namespace Slic3r::GUI::Automation
|
||||
@@ -1,31 +0,0 @@
|
||||
#include "ImGuiItemTable.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
ImGuiItemTable& ImGuiItemTable::instance() {
|
||||
static ImGuiItemTable t;
|
||||
return t;
|
||||
}
|
||||
|
||||
void ImGuiItemTable::record_item(ImGuiItemRecord rec) {
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
m_back.items.push_back(std::move(rec));
|
||||
}
|
||||
|
||||
void ImGuiItemTable::record_window(ImGuiWindowRecord rec) {
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
m_back.windows.push_back(std::move(rec));
|
||||
}
|
||||
|
||||
void ImGuiItemTable::swap_frame() {
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
m_front = std::move(m_back);
|
||||
m_back = ImGuiFrameRecord{};
|
||||
}
|
||||
|
||||
ImGuiFrameRecord ImGuiItemTable::snapshot() const {
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
return m_front;
|
||||
}
|
||||
|
||||
}}} // namespace
|
||||
@@ -1,56 +0,0 @@
|
||||
#pragma once
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
// One recorded ImGui item. Rect is in ImGui display coords; WxUiBackend maps it
|
||||
// to screen coords using the canvas client origin + DPI scale.
|
||||
struct ImGuiItemRecord {
|
||||
std::string window_name;
|
||||
std::string label; // visible label / id
|
||||
std::string type; // "button", "checkbox", "combo", "slider", "input", ...
|
||||
float x = 0, y = 0, w = 0, h = 0;
|
||||
bool enabled = true;
|
||||
bool has_value = false;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
// A complete recorded frame: items + window-level info.
|
||||
struct ImGuiWindowRecord {
|
||||
std::string name;
|
||||
float x = 0, y = 0, w = 0, h = 0;
|
||||
bool visible = true;
|
||||
};
|
||||
|
||||
struct ImGuiFrameRecord {
|
||||
std::vector<ImGuiItemRecord> items;
|
||||
std::vector<ImGuiWindowRecord> windows;
|
||||
};
|
||||
|
||||
// Double-buffered recorder. The drawing code appends to the "back" frame; render()
|
||||
// swaps it to "front" at frame end. Readers (GUI thread, after marshaling) read the
|
||||
// front frame. All access is on the GUI thread, but we guard with a mutex anyway
|
||||
// because the automation read may happen between frames.
|
||||
class ImGuiItemTable {
|
||||
public:
|
||||
static ImGuiItemTable& instance();
|
||||
|
||||
// Called from ImGuiWrapper drawing hooks (GUI thread). No-op cheap append.
|
||||
void record_item(ImGuiItemRecord rec);
|
||||
void record_window(ImGuiWindowRecord rec);
|
||||
|
||||
// Called at frame end (ImGuiWrapper::render). Promotes back -> front, clears back.
|
||||
void swap_frame();
|
||||
|
||||
// Snapshot the latest complete frame for the backend to read.
|
||||
ImGuiFrameRecord snapshot() const;
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
ImGuiFrameRecord m_back; // accumulating
|
||||
ImGuiFrameRecord m_front; // last complete
|
||||
};
|
||||
|
||||
}}} // namespace
|
||||
@@ -1,402 +0,0 @@
|
||||
#include "JsonRpcDispatcher.hpp"
|
||||
#include "WidgetSerializer.hpp"
|
||||
#include "Locator.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
JsonRpcDispatcher::JsonRpcDispatcher(IUiBackend& backend) : m_backend(backend) {}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::make_result(const nlohmann::json& id, nlohmann::json result) {
|
||||
return { {"jsonrpc","2.0"}, {"id", id}, {"result", std::move(result)} };
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::make_error(const nlohmann::json& id, int code,
|
||||
const std::string& msg) {
|
||||
return { {"jsonrpc","2.0"}, {"id", id},
|
||||
{"error", { {"code", code}, {"message", msg} }} };
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::optional<std::string> opt_str(const nlohmann::json& p, const char* key) {
|
||||
if (p.is_object() && p.contains(key) && p.at(key).is_string())
|
||||
return p.at(key).get<std::string>();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Target parse_target(const nlohmann::json& tj) {
|
||||
Target t;
|
||||
if (!tj.is_object()) return t;
|
||||
t.id = opt_str(tj, "id");
|
||||
t.path = opt_str(tj, "path");
|
||||
t.name = opt_str(tj, "name");
|
||||
t.klass = opt_str(tj, "class");
|
||||
t.label = opt_str(tj, "label");
|
||||
t.value = opt_str(tj, "value");
|
||||
if (auto b = opt_str(tj, "backend"))
|
||||
t.backend = (*b == "imgui") ? BackendKind::ImGui : BackendKind::Wx;
|
||||
return t;
|
||||
}
|
||||
|
||||
DumpOptions parse_dump_options(const nlohmann::json& p) {
|
||||
DumpOptions o;
|
||||
if (p.is_object()) {
|
||||
if (p.contains("root")) o.root = opt_str(p, "root");
|
||||
if (p.contains("max_depth") && p.at("max_depth").is_number_integer())
|
||||
o.max_depth = p.at("max_depth").get<int>();
|
||||
if (p.contains("visible_only") && p.at("visible_only").is_boolean())
|
||||
o.visible_only = p.at("visible_only").get<bool>();
|
||||
if (p.contains("include_imgui") && p.at("include_imgui").is_boolean())
|
||||
o.include_imgui = p.at("include_imgui").get<bool>();
|
||||
}
|
||||
return o;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
MouseButton parse_button(const nlohmann::json& p) {
|
||||
auto b = opt_str(p, "button");
|
||||
if (b && *b == "right") return MouseButton::Right;
|
||||
if (b && *b == "middle") return MouseButton::Middle;
|
||||
return MouseButton::Left;
|
||||
}
|
||||
|
||||
std::vector<KeyModifier> parse_modifiers(const nlohmann::json& p) {
|
||||
std::vector<KeyModifier> mods;
|
||||
if (p.is_object() && p.contains("modifiers") && p.at("modifiers").is_array()) {
|
||||
for (const auto& m : p.at("modifiers")) {
|
||||
if (!m.is_string()) continue;
|
||||
const std::string s = m.get<std::string>();
|
||||
if (s == "ctrl") mods.push_back(KeyModifier::Ctrl);
|
||||
else if (s == "shift") mods.push_back(KeyModifier::Shift);
|
||||
else if (s == "alt") mods.push_back(KeyModifier::Alt);
|
||||
else if (s == "cmd" || s == "meta") mods.push_back(KeyModifier::Cmd);
|
||||
}
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
// Parse one chord token list (already split): the last token is the key, the
|
||||
// earlier ones are modifiers.
|
||||
KeyChord chord_from_tokens(const std::vector<std::string>& tokens) {
|
||||
KeyChord c;
|
||||
for (size_t i = 0; i < tokens.size(); ++i) {
|
||||
const std::string& t = tokens[i];
|
||||
const bool is_mod = (t == "ctrl" || t == "shift" || t == "alt" ||
|
||||
t == "cmd" || t == "meta");
|
||||
if (is_mod && i + 1 < tokens.size()) {
|
||||
if (t == "ctrl") c.modifiers.push_back(KeyModifier::Ctrl);
|
||||
else if (t == "shift") c.modifiers.push_back(KeyModifier::Shift);
|
||||
else if (t == "alt") c.modifiers.push_back(KeyModifier::Alt);
|
||||
else c.modifiers.push_back(KeyModifier::Cmd);
|
||||
} else {
|
||||
c.key = t; // last token (or a lone token) is the key
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
std::vector<std::string> split(const std::string& s, char delim) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
for (char ch : s) {
|
||||
if (ch == delim) { if (!cur.empty()) out.push_back(cur); cur.clear(); }
|
||||
else cur.push_back(ch);
|
||||
}
|
||||
if (!cur.empty()) out.push_back(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
// "keys" may be a string ("ctrl+s") or an array (["ctrl","s"]). Returns one chord.
|
||||
std::vector<KeyChord> parse_keys(const nlohmann::json& params) {
|
||||
if (!params.is_object() || !params.contains("keys"))
|
||||
throw AutomationError(kInvalidParams, "input.key requires 'keys'");
|
||||
const auto& k = params.at("keys");
|
||||
std::vector<std::string> tokens;
|
||||
if (k.is_string()) {
|
||||
tokens = split(k.get<std::string>(), '+');
|
||||
} else if (k.is_array()) {
|
||||
for (const auto& e : k)
|
||||
if (e.is_string()) tokens.push_back(e.get<std::string>());
|
||||
} else {
|
||||
throw AutomationError(kInvalidParams, "'keys' must be string or array");
|
||||
}
|
||||
if (tokens.empty())
|
||||
throw AutomationError(kInvalidParams, "'keys' is empty");
|
||||
return { chord_from_tokens(tokens) };
|
||||
}
|
||||
|
||||
// "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;
|
||||
}
|
||||
|
||||
// "view" must be a non-empty string naming a top-level tab. The name->tab mapping
|
||||
// itself lives in the wx backend, so this only validates shape; throws kInvalidParams
|
||||
// when view is missing, not a string, or empty.
|
||||
std::string parse_view(const nlohmann::json& params) {
|
||||
if (!params.is_object() || !params.contains("view"))
|
||||
throw AutomationError(kInvalidParams, "view.select requires 'view'");
|
||||
const auto& v = params.at("view");
|
||||
if (!v.is_string())
|
||||
throw AutomationError(kInvalidParams, "'view' must be a string");
|
||||
std::string name = v.get<std::string>();
|
||||
if (name.empty())
|
||||
throw AutomationError(kInvalidParams, "'view' is empty");
|
||||
return name;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
std::string base64_encode(const std::vector<unsigned char>& data) {
|
||||
static const char* tbl =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string out;
|
||||
out.reserve(((data.size() + 2) / 3) * 4);
|
||||
size_t i = 0;
|
||||
for (; i + 2 < data.size(); i += 3) {
|
||||
const unsigned n = (data[i] << 16) | (data[i+1] << 8) | data[i+2];
|
||||
out.push_back(tbl[(n >> 18) & 63]);
|
||||
out.push_back(tbl[(n >> 12) & 63]);
|
||||
out.push_back(tbl[(n >> 6) & 63]);
|
||||
out.push_back(tbl[n & 63]);
|
||||
}
|
||||
if (i < data.size()) {
|
||||
unsigned n = data[i] << 16;
|
||||
const bool two = (i + 1 < data.size());
|
||||
if (two) n |= data[i+1] << 8;
|
||||
out.push_back(tbl[(n >> 18) & 63]);
|
||||
out.push_back(tbl[(n >> 12) & 63]);
|
||||
out.push_back(two ? tbl[(n >> 6) & 63] : '=');
|
||||
out.push_back('=');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
nlohmann::json image_to_json(const PngImage& img) {
|
||||
if (img.png.empty())
|
||||
throw AutomationError(kErrScreenshotFail, "screenshot produced no data");
|
||||
return { {"png_base64", base64_encode(img.png)},
|
||||
{"width", img.width}, {"height", img.height} };
|
||||
}
|
||||
} // namespace
|
||||
|
||||
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","view.select" })} };
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::dispatch(const nlohmann::json& request) {
|
||||
nlohmann::json id = request.contains("id") ? request.at("id") : nlohmann::json(nullptr);
|
||||
|
||||
if (!request.is_object() || !request.contains("method") ||
|
||||
!request.at("method").is_string()) {
|
||||
return make_error(id, kInvalidRequest, "missing or invalid 'method'");
|
||||
}
|
||||
const std::string method = request.at("method").get<std::string>();
|
||||
const nlohmann::json params =
|
||||
request.contains("params") ? request.at("params") : nlohmann::json::object();
|
||||
|
||||
try {
|
||||
if (method == "automation.version") return make_result(id, m_version(params));
|
||||
if (method == "tree.dump") return make_result(id, m_tree_dump(params));
|
||||
if (method == "tree.find") return make_result(id, m_tree_find(params));
|
||||
if (method == "widget.get") return make_result(id, m_widget_get(params));
|
||||
if (method == "input.click") return make_result(id, m_input_click(params));
|
||||
if (method == "input.type") return make_result(id, m_input_type(params));
|
||||
if (method == "input.key") return make_result(id, m_input_key(params));
|
||||
if (method == "sync.wait_for") return make_result(id, m_sync_wait_for(params));
|
||||
if (method == "app.state") return make_result(id, m_app_state(params));
|
||||
if (method == "screenshot.window") return make_result(id, m_screenshot_window(params));
|
||||
if (method == "file.open") return make_result(id, m_file_open(params));
|
||||
if (method == "view.select") return make_result(id, m_view_select(params));
|
||||
return make_error(id, kMethodNotFound, "unknown method: " + method);
|
||||
} catch (const AutomationError& e) {
|
||||
return make_error(id, e.code, e.what());
|
||||
} catch (const std::exception& e) {
|
||||
return make_error(id, kInvalidParams, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string JsonRpcDispatcher::handle_request(const std::string& body) {
|
||||
nlohmann::json req;
|
||||
try {
|
||||
req = nlohmann::json::parse(body);
|
||||
} catch (const std::exception& e) {
|
||||
return make_error(nullptr, kParseError, std::string("parse error: ") + e.what()).dump();
|
||||
}
|
||||
return dispatch(req).dump();
|
||||
}
|
||||
|
||||
// --- method handlers implemented in Tasks 7-10 (remaining stubs throw for now) ---
|
||||
nlohmann::json JsonRpcDispatcher::m_tree_dump(const nlohmann::json& params) {
|
||||
m_backend.refresh_ui();
|
||||
const UiNode root = m_backend.dump_tree(parse_dump_options(params));
|
||||
return node_to_json(root, /*include_children*/ true);
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_tree_find(const nlohmann::json& params) {
|
||||
m_backend.refresh_ui();
|
||||
const UiNode root = m_backend.dump_tree(DumpOptions{});
|
||||
const Target target =
|
||||
parse_target(params.is_object() ? params : nlohmann::json::object());
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const UiNode* n : find_matches(root, target))
|
||||
arr.push_back(node_to_json(*n, /*include_children*/ false));
|
||||
return arr;
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_widget_get(const nlohmann::json& params) {
|
||||
if (!params.is_object() || !params.contains("target"))
|
||||
throw AutomationError(kInvalidParams, "widget.get requires 'target'");
|
||||
m_backend.refresh_ui();
|
||||
const UiNode root = m_backend.dump_tree(DumpOptions{});
|
||||
int count = 0;
|
||||
const UiNode* node = resolve_unique(root, parse_target(params.at("target")), count);
|
||||
if (count == 0) throw AutomationError(kErrNotFound, "target not found");
|
||||
if (count > 1) throw AutomationError(kErrNotFound, "target is ambiguous");
|
||||
return node_to_json(*node, /*include_children*/ true);
|
||||
}
|
||||
|
||||
const UiNode JsonRpcDispatcher::resolve_actionable(const nlohmann::json& params,
|
||||
UiNode& tree_out) {
|
||||
if (!params.is_object() || !params.contains("target"))
|
||||
throw AutomationError(kInvalidParams, "missing 'target'");
|
||||
m_backend.refresh_ui();
|
||||
tree_out = m_backend.dump_tree(DumpOptions{});
|
||||
int count = 0;
|
||||
const UiNode* node = resolve_unique(tree_out, parse_target(params.at("target")), count);
|
||||
if (count == 0) throw AutomationError(kErrNotFound, "target not found");
|
||||
if (count > 1) throw AutomationError(kErrNotFound, "target is ambiguous");
|
||||
if (!node->enabled || !node->visible)
|
||||
throw AutomationError(kErrNotActionable, "target is disabled or hidden");
|
||||
return *node; // copy: stable even though tree_out outlives this call
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_input_click(const nlohmann::json& params) {
|
||||
UiNode tree;
|
||||
const UiNode node = resolve_actionable(params, tree);
|
||||
const bool dbl = params.contains("double") && params.at("double").is_boolean()
|
||||
&& params.at("double").get<bool>();
|
||||
const bool ok = m_backend.click(node, parse_button(params), dbl, parse_modifiers(params));
|
||||
return { {"ok", ok} };
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_input_type(const nlohmann::json& params) {
|
||||
if (!params.is_object() || !params.contains("text") || !params.at("text").is_string())
|
||||
throw AutomationError(kInvalidParams, "input.type requires string 'text'");
|
||||
const std::string text = params.at("text").get<std::string>();
|
||||
// Optional target: click to focus first.
|
||||
if (params.contains("target")) {
|
||||
UiNode tree;
|
||||
const UiNode node = resolve_actionable(params, tree);
|
||||
m_backend.click(node, MouseButton::Left, false, {});
|
||||
}
|
||||
const bool ok = m_backend.type_text(text);
|
||||
return { {"ok", ok} };
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_input_key(const nlohmann::json& params) {
|
||||
const bool ok = m_backend.send_keys(parse_keys(params));
|
||||
return { {"ok", ok} };
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_sync_wait_for(const nlohmann::json& params) {
|
||||
if (!params.is_object() || !params.contains("target") || !params.contains("state"))
|
||||
throw AutomationError(kInvalidParams, "sync.wait_for requires 'target' and 'state'");
|
||||
|
||||
const Target target = parse_target(params.at("target"));
|
||||
const std::string state_s = params.at("state").get<std::string>();
|
||||
WaitState state;
|
||||
if (state_s == "exists") state = WaitState::Exists;
|
||||
else if (state_s == "visible") state = WaitState::Visible;
|
||||
else if (state_s == "enabled") state = WaitState::Enabled;
|
||||
else if (state_s == "value") state = WaitState::Value;
|
||||
else throw AutomationError(kInvalidParams, "unknown state: " + state_s);
|
||||
|
||||
std::optional<std::string> expected = opt_str(params, "value");
|
||||
const int timeout_ms = params.contains("timeout_ms") && params.at("timeout_ms").is_number_integer()
|
||||
? params.at("timeout_ms").get<int>() : 5000;
|
||||
const int poll_ms = params.contains("poll_ms") && params.at("poll_ms").is_number_integer()
|
||||
? std::max(1, params.at("poll_ms").get<int>()) : 100;
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (;;) {
|
||||
m_backend.refresh_ui();
|
||||
const UiNode root = m_backend.dump_tree(DumpOptions{});
|
||||
int count = 0;
|
||||
const UiNode* node = resolve_unique(root, target, count);
|
||||
if (evaluate_state(node, state, expected)) {
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
return { {"ok", true}, {"elapsed_ms", static_cast<int>(elapsed)} };
|
||||
}
|
||||
const auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed_ms >= timeout_ms)
|
||||
throw AutomationError(kErrWaitTimeout, "wait_for timed out for state: " + state_s);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_app_state(const nlohmann::json&) {
|
||||
return app_state_to_json(m_backend.app_state());
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_screenshot_window(const nlohmann::json& params) {
|
||||
m_backend.refresh_ui();
|
||||
const UiNode* target_ptr = nullptr;
|
||||
UiNode resolved;
|
||||
if (params.is_object() && params.contains("target")) {
|
||||
UiNode tree = m_backend.dump_tree(DumpOptions{});
|
||||
int count = 0;
|
||||
const UiNode* n = resolve_unique(tree, parse_target(params.at("target")), count);
|
||||
if (count == 0) throw AutomationError(kErrNotFound, "target not found");
|
||||
if (count > 1) throw AutomationError(kErrNotFound, "target is ambiguous");
|
||||
resolved = *n;
|
||||
target_ptr = &resolved;
|
||||
}
|
||||
return image_to_json(m_backend.screenshot_window(target_ptr));
|
||||
}
|
||||
|
||||
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} };
|
||||
}
|
||||
|
||||
nlohmann::json JsonRpcDispatcher::m_view_select(const nlohmann::json& params) {
|
||||
const std::string view = parse_view(params);
|
||||
const int index = m_backend.select_view(view);
|
||||
return { {"ok", true}, {"view", view}, {"index", index} };
|
||||
}
|
||||
|
||||
}}} // namespace
|
||||
@@ -1,62 +0,0 @@
|
||||
#pragma once
|
||||
#include "IUiBackend.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
// JSON-RPC 2.0 standard error codes.
|
||||
constexpr int kParseError = -32700;
|
||||
constexpr int kInvalidRequest = -32600;
|
||||
constexpr int kMethodNotFound = -32601;
|
||||
constexpr int kInvalidParams = -32602;
|
||||
// Application error codes (design spec §5).
|
||||
constexpr int kErrNotFound = 1001; // widget/target not found (or ambiguous)
|
||||
constexpr int kErrNotActionable = 1002; // disabled / hidden
|
||||
constexpr int kErrWaitTimeout = 1003;
|
||||
constexpr int kErrGuiBusy = 1004; // GUI thread timeout
|
||||
constexpr int kErrScreenshotFail = 1005;
|
||||
constexpr int kErrDisabled = 1006;
|
||||
constexpr int kErrLoadFailed = 1007; // file.open: load_files returned empty / threw
|
||||
|
||||
constexpr const char* kProtocolVersion = "2.0";
|
||||
constexpr const char* kAutomationVersion = "1.0.0";
|
||||
|
||||
class JsonRpcDispatcher {
|
||||
public:
|
||||
explicit JsonRpcDispatcher(IUiBackend& backend);
|
||||
|
||||
// Parse a JSON-RPC request body, dispatch, and return the response body.
|
||||
// Never throws; transport-level/parse errors become JSON-RPC error responses.
|
||||
std::string handle_request(const std::string& body);
|
||||
|
||||
// For tests: dispatch an already-parsed request object and return the response.
|
||||
nlohmann::json dispatch(const nlohmann::json& request);
|
||||
|
||||
private:
|
||||
nlohmann::json make_result(const nlohmann::json& id, nlohmann::json result);
|
||||
nlohmann::json make_error(const nlohmann::json& id, int code, const std::string& msg);
|
||||
|
||||
// Method handlers (each returns the `result` object or throws AutomationError).
|
||||
nlohmann::json m_version(const nlohmann::json& params);
|
||||
nlohmann::json m_tree_dump(const nlohmann::json& params);
|
||||
nlohmann::json m_tree_find(const nlohmann::json& params);
|
||||
nlohmann::json m_widget_get(const nlohmann::json& params);
|
||||
nlohmann::json m_input_click(const nlohmann::json& params);
|
||||
nlohmann::json m_input_type(const nlohmann::json& params);
|
||||
nlohmann::json m_input_key(const nlohmann::json& params);
|
||||
nlohmann::json m_sync_wait_for(const nlohmann::json& params);
|
||||
nlohmann::json m_app_state(const nlohmann::json& params);
|
||||
nlohmann::json m_screenshot_window(const nlohmann::json& params);
|
||||
nlohmann::json m_file_open(const nlohmann::json& params);
|
||||
nlohmann::json m_view_select(const nlohmann::json& params);
|
||||
|
||||
// Resolve a unique, actionable (enabled+visible) node from params["target"].
|
||||
// Throws kErrNotFound (missing/ambiguous) or kErrNotActionable (disabled/hidden).
|
||||
// `tree_out` keeps the snapshot alive; the returned node is a stable copy.
|
||||
const UiNode resolve_actionable(const nlohmann::json& params, UiNode& tree_out);
|
||||
|
||||
IUiBackend& m_backend;
|
||||
};
|
||||
|
||||
}}} // namespace
|
||||
@@ -1,68 +0,0 @@
|
||||
#include "Locator.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
static void flatten_into(const UiNode& n, std::vector<const UiNode*>& out) {
|
||||
out.push_back(&n);
|
||||
for (const UiNode& c : n.children)
|
||||
flatten_into(c, out);
|
||||
}
|
||||
|
||||
std::vector<const UiNode*> flatten(const UiNode& root) {
|
||||
std::vector<const UiNode*> out;
|
||||
flatten_into(root, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool matches_predicate(const UiNode& n, const Target& t) {
|
||||
if (t.backend && n.backend != *t.backend) return false;
|
||||
if (t.name && !(n.id == *t.name || n.label == *t.name)) return false;
|
||||
if (t.klass && n.klass != *t.klass) return false;
|
||||
if (t.label && n.label != *t.label) return false;
|
||||
if (t.value && !(n.has_value && n.value == *t.value)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<const UiNode*> find_matches(const UiNode& root, const Target& target) {
|
||||
const auto all = flatten(root);
|
||||
std::vector<const UiNode*> out;
|
||||
|
||||
// Resolution order: exact id -> exact path -> predicate.
|
||||
if (target.id) {
|
||||
for (const UiNode* n : all)
|
||||
if (n->id == *target.id) out.push_back(n);
|
||||
return out;
|
||||
}
|
||||
if (target.path) {
|
||||
for (const UiNode* n : all)
|
||||
if (n->path == *target.path) out.push_back(n);
|
||||
return out;
|
||||
}
|
||||
if (target.empty())
|
||||
return out; // nothing to match on
|
||||
for (const UiNode* n : all)
|
||||
if (matches_predicate(*n, target)) out.push_back(n);
|
||||
return out;
|
||||
}
|
||||
|
||||
const UiNode* resolve_unique(const UiNode& root, const Target& target, int& match_count) {
|
||||
const auto m = find_matches(root, target);
|
||||
match_count = static_cast<int>(m.size());
|
||||
return m.size() == 1 ? m.front() : nullptr;
|
||||
}
|
||||
|
||||
bool evaluate_state(const UiNode* node, WaitState state,
|
||||
const std::optional<std::string>& expected_value) {
|
||||
if (node == nullptr)
|
||||
return false;
|
||||
switch (state) {
|
||||
case WaitState::Exists: return true;
|
||||
case WaitState::Visible: return node->visible;
|
||||
case WaitState::Enabled: return node->enabled && node->visible;
|
||||
case WaitState::Value:
|
||||
return node->has_value && expected_value && node->value == *expected_value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}}} // namespace
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user