mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-06 02:21:21 +00:00
1017 lines
51 KiB
Markdown
1017 lines
51 KiB
Markdown
# Plugin Development
|
||
|
||
OrcaSlicer can be extended with **Python plugins** that run inside an embedded CPython
|
||
interpreter, without recompiling the application. This document is for two audiences:
|
||
|
||
1. **Plugin authors** — writing a new Python plugin, modifying an existing one, and
|
||
debugging it during development.
|
||
2. **OrcaSlicer contributors** — adding a brand‑new *plugin type* in C++ (a new contract
|
||
that Python plugins can implement and that the app invokes at some point in its
|
||
workflow).
|
||
|
||
Two companion documents go deeper on adjacent topics; read them alongside this one:
|
||
|
||
- [`plugin_audit_hook.md`](plugin_audit_hook.md) — the CPython audit hook that restricts
|
||
what plugin code may do (today: a filesystem write allow‑list). **Anyone adding a new
|
||
trampoline method must read it** — every C++→Python call must choose an audit mode.
|
||
- [`plugin_system.md`](plugin_system.md) — the catalog/loader/cloud‑subscription side of the
|
||
system (discovery, install, update).
|
||
|
||
> **All file paths below are under `src/slic3r/plugin/`** unless stated otherwise.
|
||
|
||
---
|
||
|
||
## Part 1 — Python Plugin Development
|
||
|
||
### Where plugins live and how they are discovered
|
||
|
||
Plugins are loaded from two roots under the OrcaSlicer data directory (`data_dir()`):
|
||
|
||
| Root | Purpose |
|
||
|---|---|
|
||
| `data_dir()/orca_plugins/` | locally installed / side‑loaded plugins |
|
||
| `data_dir()/orca_plugins/_subscribed/<user_id>/` | cloud‑subscribed plugins |
|
||
|
||
Each plugin lives in **its own subdirectory** containing exactly one entry file — either a
|
||
single `.py` file or a single `.whl` (wheel). Subdirectories whose name starts with `.` or
|
||
`__` are ignored. Discovery is driven by `PluginCatalog` (scan) and `PluginLoader` (load);
|
||
see `PluginCatalog.cpp` and `PluginLoader.cpp` (`find_installed_plugin_entry` in
|
||
`PythonFileUtils.cpp` decides which file in a folder is the entry point).
|
||
|
||
> There are **no bundled example plugins in the repository.** The plugin snippets in this
|
||
> document are illustrative — they were written against the real bindings below, but are
|
||
> not copied from a shipped, verified plugin. Treat them as starting points and test them.
|
||
|
||
### Anatomy of a plugin
|
||
|
||
A plugin is packaged in **one of two forms**, and the entry file in its folder is what
|
||
distinguishes them:
|
||
|
||
- **A single `.py` file** — the simplest form, covered first below. Metadata lives in a
|
||
PEP 723 comment block at the top of the file.
|
||
- **A wheel (`.whl`)** — a normal built Python package, for plugins that need multiple
|
||
modules or compiled code. Metadata comes from the wheel's own files instead of a PEP 723
|
||
block. See [Wheel (`.whl`) plugins](#wheel-whl-plugins) below.
|
||
|
||
**One plugin, many capabilities.** A plugin is a *package* that registers one or more
|
||
**capabilities**. Each capability is a single typed unit of functionality — a script you can
|
||
run, a G‑code post‑processor, a printer agent — with its own display name. A plugin's *types*
|
||
are derived from the capabilities it registers (they are descriptive tags, not a single fixed
|
||
role), so one plugin can, for example, offer both a script capability and a post‑processing
|
||
capability at once.
|
||
|
||
**Both forms register the same way**: OrcaSlicer imports your code, instantiates the one
|
||
**package class** you marked with `@orca.plugin`, and calls its `register_capabilities()`
|
||
method to collect the capabilities it offers. Only the packaging and the metadata source
|
||
differ.
|
||
|
||
A single‑file (`.py`) plugin has three parts:
|
||
|
||
1. A **PEP 723 inline metadata block** (a special comment header) declaring identity and
|
||
dependencies.
|
||
2. One or more **capability classes**, each subclassing a typed base exposed by the embedded
|
||
`orca` module (a script, G‑code/post‑processing, or printer‑agent capability) and
|
||
implementing `get_name()` plus its entry method.
|
||
3. A **package class** decorated with `@orca.plugin` (subclassing `orca.base`) whose
|
||
`register_capabilities()` method calls `orca.register_capability(...)` once per capability.
|
||
|
||
#### 1. The metadata block (PEP 723)
|
||
|
||
OrcaSlicer reads identity and dependency metadata from a PEP 723 *inline script metadata*
|
||
block — a comment block delimited by `# /// script` and `# ///`, with each content line
|
||
prefixed by `# `. Identity fields live in a `[tool.orcaslicer.plugin]` table; dependency
|
||
fields live at the TOML root. Parsing is implemented in
|
||
`PythonFileUtils.cpp::parse_pep723_toml` / `read_python_plugin_metadata`.
|
||
|
||
```python
|
||
# /// script
|
||
# requires-python = ">=3.12"
|
||
# dependencies = []
|
||
#
|
||
# [tool.orcaslicer.plugin]
|
||
# name = "Sample Plugin"
|
||
# description = "Appends a short build-environment note to the exported G-code."
|
||
# author = "Your Name"
|
||
# version = "1.0.0"
|
||
# ///
|
||
```
|
||
|
||
| Field | Location | Required | Notes |
|
||
|---|---|---|---|
|
||
| `name` | `[tool.orcaslicer.plugin]` | recommended | display name in the Plugins dialog |
|
||
| `description` | `[tool.orcaslicer.plugin]` | recommended | shown in the plugin **Description** tab |
|
||
| `author` | `[tool.orcaslicer.plugin]` | optional | |
|
||
| `version` | `[tool.orcaslicer.plugin]` | recommended | |
|
||
| `dependencies` | TOML root | optional | array of pip requirements (see [Dependencies](#dependencies)) |
|
||
| `requires-python` | TOML root | optional | **read but not stored or enforced** against the bundled interpreter today |
|
||
|
||
> **The metadata block no longer declares a `type`.** A plugin's type(s) are derived from the
|
||
> capability classes it registers (each capability's `get_type()`), so the same metadata block
|
||
> is used whether the plugin offers one capability or several. The PEP 723 parser
|
||
> (`parse_pep723_toml`) reads only `name`, `description`, `author`, `version`,
|
||
> `requires-python`, and `dependencies`; any other key (including a stray `type = …`) is
|
||
> ignored. The same applies to `.whl` plugins — identity comes from the wheel's `METADATA`,
|
||
> and the served types come from the registered capabilities.
|
||
|
||
#### 2. The `orca` module — the plugin API
|
||
|
||
The interpreter exposes a single embedded module named **`orca`**
|
||
(`PYBIND11_EMBEDDED_MODULE(orca, ...)` in `PythonPluginBridge.cpp`). It provides the capability
|
||
base classes, the package base, and capability registration, plus the **`orca.host`** submodule
|
||
for read-only access to the live slicer model graph, presets, and mesh geometry (see
|
||
[The `orca.host` module](#the-orcahost-module--read-only-host-access)). It contains:
|
||
|
||
| Symbol | Kind | Members / purpose |
|
||
|---|---|---|
|
||
| `orca.PluginType` | enum | `PostProcessing`, `PrinterConnection`, `Automation`, `Analysis`, `Importer`, `Exporter`, `Visualization`, `Script`, `Unknown` |
|
||
| `orca.PluginResult` | enum | `Success`, `Skipped`, `RecoverableError`, `FatalError` |
|
||
| `orca.PluginContext` | class | base context, field `orca_version: str` |
|
||
| `orca.ExecutionResult` | class | fields `status`, `message`, `data`; factories below |
|
||
| `orca.PythonPluginBase` | class | the root **capability** base; subclasses must implement `get_name()` |
|
||
| `orca.base` | class | the **package** base; subclass it and override `register_capabilities()` |
|
||
| `orca.plugin` | decorator | marks the single package class for the file (exactly one per file) |
|
||
| `orca.register_capability(cls)` | function | register one capability class; call it inside `register_capabilities()` |
|
||
| `orca.gcode` | submodule | `GCodePluginContext`, `GCodePluginCapabilityBase` |
|
||
| `orca.script` | submodule | `ScriptPluginCapabilityBase` |
|
||
| `orca.printer_agent` | submodule | `PrinterAgentBase` and its data types |
|
||
| `orca.host` | submodule | read-only host access: live `Model` graph, presets/bundle, and zero-copy mesh geometry |
|
||
|
||
`ExecutionResult` is how a plugin reports the outcome of a run:
|
||
|
||
```python
|
||
orca.ExecutionResult.success(message="", data="")
|
||
orca.ExecutionResult.skipped(message="")
|
||
orca.ExecutionResult.failure(status, message, data="") # status is an orca.PluginResult
|
||
```
|
||
|
||
- `status` — an `orca.PluginResult`.
|
||
- `message` — human‑readable text; this is what surfaces in error/result dialogs.
|
||
- `data` — a free‑form string whose meaning is defined by the plugin/workflow (not
|
||
interpreted by the framework).
|
||
|
||
#### The `orca.host` module — read-only host access
|
||
|
||
`orca.host` (bound in `PluginHostApi.cpp`) gives plugins **read-only** access to the running
|
||
slicer. It is intended for analysis, reporting, and export plugins; nothing here mutates the
|
||
model. **Script plugins run on the main/UI thread**, so within one `execute()` the model cannot
|
||
change under you; **G-code/post-processing and printer-agent plugins run on a background thread**
|
||
while the GUI keeps running. Either way, treat everything as a momentary snapshot and do not stash
|
||
references across runs.
|
||
|
||
**Entry points** (each raises `RuntimeError` if called before the GUI/model is ready):
|
||
|
||
```python
|
||
import orca
|
||
model = orca.host.model() # the active Model
|
||
plater = orca.host.plater() # the Plater
|
||
bundle = orca.host.preset_bundle() # presets (prints/printers/filaments/...)
|
||
```
|
||
|
||
**Model graph:** `Model.objects()` → `ModelObject`; each object has `volumes()`/`volume(i)`
|
||
(→ `ModelVolume`) and `instances()`/`instance(i)` (→ `ModelInstance`). Bounding boxes are a
|
||
`host.BoundingBox` value type (`min`/`max`/`size`/`center` as `(x, y, z)` mm tuples, plus
|
||
`radius`/`defined`).
|
||
|
||
**Mesh geometry — `ModelVolume.mesh()` → `host.TriangleMesh`:**
|
||
|
||
| Member | Returns | Notes |
|
||
|---|---|---|
|
||
| `vertex_count()` / `triangle_count()` (`facets_count()`) / `is_empty()` | `int` / `bool` | numpy-free |
|
||
| `vertex(i)` / `triangle(i)` | `(x, y, z)` / `(a, b, c)` tuple | numpy-free, bounds-checked |
|
||
| `vertices()` | `(N, 3)` float32 ndarray | **read-only, zero-copy**, requires numpy |
|
||
| `triangles()` | `(M, 3)` int32 ndarray | vertex indices; **read-only, zero-copy**, requires numpy |
|
||
| `face_normals()` | `(M, 3)` float32 ndarray | computed copy, requires numpy |
|
||
| `volume()` / `bounding_box()` / `is_manifold()` | `float` / `BoundingBox` / `bool` | numpy-free |
|
||
|
||
Coordinates are **local** (the volume's own frame, in mm). The `vertices()`/`triangles()`
|
||
arrays are zero-copy views into the live mesh and are marked read-only — writing to them raises
|
||
`ValueError`. Their lifetime is pinned to an immutable mesh snapshot, so they stay valid even if
|
||
the volume's mesh is later replaced.
|
||
|
||
**Worked example** (declare numpy in the PEP 723 block so the bundled `uv` installs it):
|
||
|
||
```python
|
||
# /// script
|
||
# dependencies = ["numpy"]
|
||
# ///
|
||
import orca, numpy as np
|
||
|
||
class MeshReport(orca.script.ScriptPluginCapabilityBase):
|
||
def get_name(self): return "Mesh Report"
|
||
def execute(self):
|
||
model = orca.host.model()
|
||
for obj in model.objects():
|
||
for vol in obj.volumes():
|
||
mesh = vol.mesh()
|
||
V = np.asarray(mesh.vertices()) # (N, 3) float32, read-only
|
||
T = np.asarray(mesh.triangles()) # (M, 3) int32
|
||
# World-space coordinates for the first instance (row-vector convention):
|
||
M = obj.instance(0).matrix() @ vol.matrix() # 4x4 float64
|
||
world = (np.c_[V.astype(np.float64), np.ones(len(V))] @ M.T)[:, :3]
|
||
print(vol.name, V.shape, T.shape, world.min(0), world.max(0))
|
||
return orca.ExecutionResult.success()
|
||
|
||
@orca.plugin
|
||
class MeshReportPlugin(orca.base):
|
||
def register_capabilities(self):
|
||
orca.register_capability(MeshReport)
|
||
```
|
||
|
||
> If the instance is mirrored (`instance.is_left_handed()` is `True`, i.e. `det(M) < 0`), flip
|
||
> triangle winding / negate face normals when computing outward-facing normals in world space.
|
||
|
||
**numpy requirement:** `vertices()`, `triangles()`, `face_normals()`, and the `matrix()`
|
||
accessors on `ModelVolume`/`ModelInstance` require numpy and raise a clear `ImportError` if it
|
||
is not installed (declare `dependencies = ["numpy"]`). Everything else — counts, `vertex(i)`/
|
||
`triangle(i)`, `volume()`, `bounding_box()`, `is_manifold()`, and the `offset`/`rotation`/
|
||
`scaling_factor`/`mirror` tuple accessors — works without numpy.
|
||
|
||
#### The `orca.host.ui` module — dialogs and interactive windows
|
||
|
||
`orca.host.ui` lets a plugin show host‑owned UI: a native message box, a native progress
|
||
dialog, a modal HTML dialog, and non‑modal interactive windows. **A plugin must never import
|
||
its own GUI toolkit**
|
||
(PyQt/wxPython/tkinter): a `script` plugin shares the host's UI thread, so a second toolkit's
|
||
event loop would clash with wxWidgets, and a `gcode`/`printer-agent` plugin runs off the main
|
||
thread where toolkit calls would crash. These host calls run on the main thread for you and
|
||
block the calling code until they return.
|
||
|
||
```python
|
||
# Native message box -> returns "ok" | "cancel" | "yes" | "no"
|
||
choice = orca.host.ui.message("Export finished. Open the folder?",
|
||
title="My Plugin", buttons="yes_no", icon="question")
|
||
|
||
# Modal HTML dialog -> returns the orca.submit() payload (dict), or None if dismissed
|
||
result = orca.host.ui.show_dialog(html="<h2>Hello</h2> ...", title="Report",
|
||
width=820, height=600)
|
||
|
||
# Non-modal, persistent, interactive window -> returns a UiWindow handle
|
||
win = orca.host.ui.create_window(html=PAGE, title="Panel",
|
||
on_message=self.on_message, on_close=self.on_close)
|
||
win.post({"type": "data", "rows": [...]}) # push a payload to the page
|
||
win.is_open() # bool
|
||
win.close()
|
||
```
|
||
|
||
`message` arguments: `buttons` is `"ok"|"ok_cancel"|"yes_no"|"yes_no_cancel"`; `icon` is
|
||
`"info"|"warning"|"error"|"question"`.
|
||
|
||
**Progress dialogs:**
|
||
|
||
Use `create_progress_dialog()` for host-owned native progress. It returns a
|
||
`ProgressDialog` handle and also works as a context manager, so `close()` is called on exit.
|
||
The default style is `PD_APP_MODAL | PD_AUTO_HIDE`; add `PD_CAN_ABORT` if the user should be
|
||
able to cancel. `maximum` defaults to `100` (values `<= 0` are treated as `100`).
|
||
|
||
For script plugins, put the dialog inside `execute(self)` and update it between chunks of
|
||
work. Do not create the dialog and then run one long uninterrupted operation such as a single
|
||
`time.sleep(...)` or blocking network call; the dialog only gets useful repaint/cancel
|
||
checkpoints when you call `update()` or `pulse()`.
|
||
|
||
```python
|
||
style = (orca.host.ui.PD_APP_MODAL |
|
||
orca.host.ui.PD_AUTO_HIDE |
|
||
orca.host.ui.PD_CAN_ABORT |
|
||
orca.host.ui.PD_ELAPSED_TIME |
|
||
orca.host.ui.PD_REMAINING_TIME)
|
||
|
||
with orca.host.ui.create_progress_dialog("My Plugin",
|
||
"Preparing...",
|
||
maximum=len(items),
|
||
style=style) as progress:
|
||
for index, item in enumerate(items, start=1):
|
||
process(item)
|
||
|
||
# update() returns False if the dialog was closed or cancelled.
|
||
if not progress.update(index, f"Processed {index}/{len(items)}"):
|
||
return orca.ExecutionResult.skipped("Cancelled by user")
|
||
```
|
||
|
||
For indeterminate work, pulse the dialog instead of setting a numeric value:
|
||
|
||
```python
|
||
with orca.host.ui.create_progress_dialog("My Plugin",
|
||
"Waiting for printer...",
|
||
style=orca.host.ui.PD_APP_MODAL |
|
||
orca.host.ui.PD_CAN_ABORT) as progress:
|
||
while not finished():
|
||
if not progress.pulse("Waiting for printer..."):
|
||
return orca.ExecutionResult.skipped("Cancelled by user")
|
||
wait_for_next_poll()
|
||
```
|
||
|
||
Handle methods:
|
||
|
||
| Python call | Effect |
|
||
|---|---|
|
||
| `progress.update(value, message="")` | set the determinate progress value; returns `False` if closed/cancelled |
|
||
| `progress.pulse(message="")` | advance an indeterminate progress step; returns `False` if closed/cancelled |
|
||
| `progress.start_pulse(interval_ms=100, message="")` | start timer-driven pulsing on the UI thread |
|
||
| `progress.stop_pulse()` | stop timer-driven pulsing |
|
||
| `progress.close()` | close the dialog |
|
||
| `progress.is_open()` | return whether the host still has the dialog registered |
|
||
|
||
Because `start_pulse()` has no return value, use explicit `update()` or `pulse()` calls at
|
||
natural cancellation points if the dialog includes `PD_CAN_ABORT`.
|
||
|
||
The style constants exposed by `orca.host.ui` mirror `wxProgressDialog`: `PD_APP_MODAL`,
|
||
`PD_AUTO_HIDE`, `PD_CAN_ABORT`, `PD_CAN_SKIP`, `PD_ELAPSED_TIME`, `PD_ESTIMATED_TIME`, and
|
||
`PD_REMAINING_TIME`. `PD_CAN_SKIP` is available for style parity, but the current Python
|
||
handle does not expose a separate "skip" state.
|
||
|
||
**The page talks back through `window.orca`** (injected automatically; the page supplies raw,
|
||
self‑contained HTML/CSS/JS):
|
||
|
||
| JS call | Effect |
|
||
|---|---|
|
||
| `orca.postMessage(obj)` | deliver `obj` to the plugin's `on_message(obj)` |
|
||
| `orca.onMessage(cb)` | `cb(data)` runs for each `win.post(data)` (and modal pushes) |
|
||
| `orca.submit(obj)` | (modal) close and return `obj` from `show_dialog` |
|
||
| `orca.close()` | close the dialog / window |
|
||
|
||
**Theming (automatic light/dark):**
|
||
|
||
The host injects a stylesheet that matches OrcaSlicer's **current theme** (the active
|
||
light/dark mode, fonts, background/foreground, accent and border colors) *before* your page
|
||
renders. An unstyled page already looks native — `<body>`, headings, `button`,
|
||
`input`/`select`/`textarea`, `table`, links and scrollbars get sensible themed defaults — and
|
||
the theme is also exposed as CSS variables so you can match the rest of the UI:
|
||
|
||
| Variable | Meaning |
|
||
|---|---|
|
||
| `--orca-bg` | window/background color |
|
||
| `--orca-fg` | primary text color |
|
||
| `--orca-muted` | secondary / label text color |
|
||
| `--orca-accent` | accent color (buttons, links, focus) |
|
||
| `--orca-accent-fg` | text color on the accent |
|
||
| `--orca-border` | subtle border / separator / row‑hover color |
|
||
| `--orca-font` | UI font stack |
|
||
|
||
The injected rules use only low specificity and never `!important`, so **any CSS your page
|
||
ships overrides them**. Prefer the variables (e.g. `border:1px solid var(--orca-border)`) over
|
||
hardcoded colors so your dialog follows light *and* dark mode automatically. The UI sample
|
||
([`host_ui_panel.py`](examples/host_ui_panel.py)) relies on this and uses no fixed colors.
|
||
|
||
**Threading & lifecycle:**
|
||
|
||
- Host UI calls run on the main thread and **block the calling code** until they return
|
||
(`message`/`show_dialog` when the dialog closes; `create_window`/`create_progress_dialog`
|
||
as soon as the window/dialog is shown; progress updates after the host applies them). From
|
||
a `script` plugin — already on the UI thread — they run inline; from a background-thread
|
||
plugin (`gcode`/`printer-agent`) they marshal to the main thread first.
|
||
- `on_message(data)` runs on the **UI thread** — keep it quick; offload heavy work to a
|
||
`threading.Thread` and push results back with `win.post(...)`.
|
||
- A **modal** dialog (`show_dialog`) fits a one‑shot `execute()`. A **persistent** panel
|
||
(`create_window`) is best opened from `on_load()` so it lives for the plugin's lifetime; the
|
||
host closes a plugin's windows automatically when it is unloaded/reloaded or the app exits.
|
||
- Content is loaded as raw HTML — prefer **self‑contained** pages (inline CSS/JS). There is no
|
||
CSP and developer tools are disabled.
|
||
|
||
See [`examples/host_ui_panel.py`](examples/host_ui_panel.py) for a non‑modal interactive panel
|
||
that browses the whole `orca.host` read-only API.
|
||
|
||
#### 3. Registration
|
||
|
||
Registration has two parts, both resolved at **module import / load time**.
|
||
|
||
**Capabilities** — each capability is a class that subclasses a typed base (see
|
||
[Capability types and entry points](#capability-types-and-entry-points)) and implements
|
||
`get_name(self) -> str`. The name is how the capability appears in the UI and how presets
|
||
refer to it, so it must be **unique within the plugin** and **must not contain a `;`** — that
|
||
character is reserved as a separator in preset references, and a `;` in a capability name
|
||
fails the load.
|
||
|
||
**The package** — exactly one class per file is decorated with `@orca.plugin` and subclasses
|
||
`orca.base`. Its `register_capabilities(self)` method calls `orca.register_capability(Cls)`
|
||
once for each capability class you want to expose:
|
||
|
||
```python
|
||
@orca.plugin
|
||
class SamplePlugin(orca.base):
|
||
def register_capabilities(self):
|
||
orca.register_capability(GCodeBenchmark)
|
||
orca.register_capability(EnvironmentReport)
|
||
```
|
||
|
||
OrcaSlicer instantiates the package class (it must be callable as `SamplePlugin()` with no
|
||
arguments), calls `register_capabilities()`, then instantiates each registered capability.
|
||
|
||
Rules enforced when a plugin loads (most in `PythonPluginBridge.cpp`):
|
||
|
||
- The `@orca.plugin` class **must** subclass `orca.base`, and there must be **exactly one**
|
||
per file — a second `@orca.plugin` fails the load.
|
||
- Each class passed to `orca.register_capability` must subclass a capability base (ultimately
|
||
`orca.PythonPluginBase`); otherwise it raises `value_error`.
|
||
- Every capability must resolve `get_name()` (checked in the bridge); the loader
|
||
(`PluginLoader.cpp`) additionally rejects the plugin if the resulting `(type, name)` pair is
|
||
not unique across it.
|
||
- A capability class you never pass to `register_capability` is **invisible** to OrcaSlicer,
|
||
even if it is defined in the file.
|
||
|
||
#### Wheel (`.whl`) plugins
|
||
|
||
For anything beyond a single file — multiple modules, packaged resources, or compiled
|
||
extensions — ship a standard Python **wheel** as the plugin folder's entry file. The plugin
|
||
*code* is identical to the `.py` case: somewhere in the importable package's top‑level code
|
||
(typically its `__init__.py`) you define your capability classes and the `@orca.plugin`
|
||
package class that registers them. What changes is **where metadata comes from** and that the
|
||
wheel is validated on install (`read_wheel_plugin_metadata` in `PythonFileUtils.cpp`).
|
||
|
||
Identity and dependencies are read from the wheel's `*.dist-info/` files instead of a
|
||
PEP 723 block:
|
||
|
||
| Plugin field | Wheel source |
|
||
|---|---|
|
||
| `name` | `METADATA` → `Name` (**required**) |
|
||
| `version` | `METADATA` → `Version` (**required**) |
|
||
| `description` | `METADATA` → `Summary` |
|
||
| `author` | `METADATA` → `Author` |
|
||
| `dependencies` | `METADATA` → `Requires-Dist` |
|
||
|
||
As with `.py` plugins, the wheel does **not** declare a type — the plugin's served types come
|
||
from the capabilities its `@orca.plugin` package class registers at load time.
|
||
|
||
Additional wheel rules enforced at install time:
|
||
|
||
- The wheel must contain exactly **one `.dist-info` directory** with `METADATA`, `WHEEL`,
|
||
and `RECORD` present.
|
||
- **Platform/ABI compatibility is checked** from the `WHEEL` file's `Tag:` lines. Pure
|
||
Python wheels (`*-none-any`) are accepted everywhere; platform‑specific wheels must match
|
||
the current interpreter's ABI tag and OS (see `PythonInterpreter::python_abi_tag()`).
|
||
Ship a pure‑Python wheel unless you genuinely need a compiled extension.
|
||
- The importable entry package is chosen in priority order: core‑metadata `Import-Name`,
|
||
then `top_level.txt` (if it names a single package), then the normalized `Name`.
|
||
|
||
### Capability types and entry points
|
||
|
||
Each typed base defines the method(s) OrcaSlicer will call and the type returned by
|
||
`get_type()`. Every capability **must** implement `get_name(self) -> str`. Lifecycle hooks
|
||
`on_load()` / `on_unload()` are optional and available on every capability (defaults do
|
||
nothing).
|
||
|
||
| Base class | `get_type()` returns | Required methods | Invoked by |
|
||
|---|---|---|---|
|
||
| `orca.script.ScriptPluginCapabilityBase` | `Script` | `get_name()`, `execute(self) -> ExecutionResult` | the **Plugins dialog → Run** action |
|
||
| `orca.gcode.GCodePluginCapabilityBase` | `PostProcessing` | `get_name()`, `execute(self, ctx) -> ExecutionResult` | **G‑code export / post‑processing** during slicing |
|
||
| `orca.printer_agent.PrinterAgentBase` | `PrinterConnection` | `get_name()` + ~30 agent methods (`get_agent_info`, `connect_printer`, …) | the **network / printer‑agent** layer on load |
|
||
|
||
> **`get_name()` is required; `get_type()` usually isn't.** Every capability must implement
|
||
> `get_name()` — it is pure virtual on the root base, and a missing override fails the load.
|
||
> The typed C++ bases already implement `get_type()` (e.g. `ScriptPluginCapability::get_type()`
|
||
> returns `Script`), so a subclass of a *typed* base does **not** need to override it. Only a
|
||
> capability that subclasses the **root** `orca.PythonPluginBase` directly must set its own
|
||
> `get_type()`.
|
||
|
||
> **Threading.** `ScriptPluginCapabilityBase.execute()` runs on the **main/UI thread**: live
|
||
> host handles are safe to read for the whole call and `orca.host.ui` dialogs open inline, but a
|
||
> slow `execute()` **freezes the UI**. Keep it quick — offload heavy work to your own
|
||
> `threading.Thread` (which must not touch the model) and surface results through a
|
||
> `create_window` panel. `GCodePluginCapabilityBase` / `PrinterAgentBase` instead run on
|
||
> background (slicing / network) threads.
|
||
|
||
The G‑code context (`orca.gcode.GCodePluginContext`) is passed to `execute` and exposes
|
||
read/write fields:
|
||
|
||
| Field | Meaning |
|
||
|---|---|
|
||
| `orca_version` | OrcaSlicer version string (inherited from `PluginContext`) |
|
||
| `gcode_path` | absolute path to the temporary G‑code file being post‑processed |
|
||
| `host` | target host, when exporting to a network printer |
|
||
| `output_name` | the output file name |
|
||
|
||
> **Filesystem access is audited.** While `execute()` runs, the audit hook restricts
|
||
> writes to an allow‑list. G‑code plugins additionally get the folder containing
|
||
> `gcode_path` added as a scoped writable root, so appending to / rewriting the current
|
||
> G‑code file is allowed; writing elsewhere outside `data_dir()` is blocked. See
|
||
> [`plugin_audit_hook.md`](plugin_audit_hook.md).
|
||
|
||
### Complete examples
|
||
|
||
**Minimal script plugin** — one capability, runs from the Plugins dialog, no context:
|
||
|
||
```python
|
||
# /// script
|
||
# [tool.orcaslicer.plugin]
|
||
# name = "Hello Script"
|
||
# description = "Smallest possible script plugin."
|
||
# author = "Your Name"
|
||
# version = "1.0.0"
|
||
# ///
|
||
import orca
|
||
|
||
|
||
class HelloScript(orca.script.ScriptPluginCapabilityBase):
|
||
def get_name(self):
|
||
return "Hello Script"
|
||
|
||
def on_load(self):
|
||
# Optional: runs once when the capability is loaded.
|
||
pass
|
||
|
||
def execute(self):
|
||
return orca.ExecutionResult.success("Hello from a script plugin")
|
||
|
||
|
||
@orca.plugin
|
||
class HelloPlugin(orca.base):
|
||
def register_capabilities(self):
|
||
orca.register_capability(HelloScript)
|
||
```
|
||
|
||
**Multi-capability plugin** — one package that exposes a post‑processing capability *and* a
|
||
script capability:
|
||
|
||
```python
|
||
# /// script
|
||
# [tool.orcaslicer.plugin]
|
||
# name = "Sample Plugin"
|
||
# description = "Demonstrates registering several capabilities from one plugin."
|
||
# author = "Your Name"
|
||
# version = "1.0.0"
|
||
# ///
|
||
import orca
|
||
|
||
|
||
class EnvironmentReport(orca.gcode.GCodePluginCapabilityBase):
|
||
def get_name(self):
|
||
return "Environment Report"
|
||
|
||
def execute(self, ctx):
|
||
# ctx.gcode_path / ctx.output_name / ctx.host / ctx.orca_version are available.
|
||
# Writing to the current G-code file's folder is permitted by the audit hook.
|
||
try:
|
||
with open(ctx.gcode_path, "a", encoding="utf-8") as f:
|
||
f.write(f"\n; processed by Environment Report for {ctx.output_name}\n")
|
||
except Exception as exc:
|
||
return orca.ExecutionResult.failure(
|
||
orca.PluginResult.RecoverableError,
|
||
f"could not append report: {exc}")
|
||
return orca.ExecutionResult.success("report appended")
|
||
|
||
|
||
class GCodeBenchmark(orca.script.ScriptPluginCapabilityBase):
|
||
def get_name(self):
|
||
return "G-code Benchmark"
|
||
|
||
def execute(self):
|
||
return orca.ExecutionResult.success("benchmark complete")
|
||
|
||
|
||
@orca.plugin
|
||
class SamplePlugin(orca.base):
|
||
def register_capabilities(self):
|
||
orca.register_capability(EnvironmentReport)
|
||
orca.register_capability(GCodeBenchmark)
|
||
```
|
||
|
||
For a copy‑pasteable starter that registers a script, a post‑processing, and a printer‑agent
|
||
capability in one package, see
|
||
[`examples/multi_capability_skeleton.py`](examples/multi_capability_skeleton.py).
|
||
|
||
> **Capability names and presets.** When a capability is chosen for a setting (for example a
|
||
> post‑processing capability), its `get_name()` is what the preset stores. The full reference
|
||
> saved alongside it is `<plugin_name>;<cloud_uuid>;<capability_name>` — which is why a
|
||
> capability name may not contain `;`. See
|
||
> [Plugin references in presets](plugin_system.md#plugin-references-in-presets) for how this
|
||
> is used to restore missing plugins.
|
||
|
||
### Dependencies
|
||
|
||
List third‑party requirements in the PEP 723 root `dependencies` array. On install,
|
||
OrcaSlicer resolves them with a bundled `uv` into the plugin's environment
|
||
(`PluginLoader.cpp`):
|
||
|
||
```python
|
||
# dependencies = ["requests==2.32.3", "humanize"]
|
||
```
|
||
|
||
Keep dependencies minimal — every dependency is code that runs under the same audit policy
|
||
as your plugin and must be fetched at install time.
|
||
|
||
### Modifying an existing plugin
|
||
|
||
1. Locate its folder under `data_dir()/orca_plugins/<plugin>/` (or, for subscribed plugins,
|
||
under `.../_subscribed/<user_id>/`).
|
||
2. Edit the `.py` entry file. If you change the metadata block, bump `version` so the change
|
||
is visible in the Plugins dialog.
|
||
3. Reload (see the iteration workflow below). Note that plugin instances are captured at
|
||
load time — a running OrcaSlicer will not pick up source edits until the plugin is
|
||
reloaded or the app is restarted.
|
||
|
||
> Editing a `.whl` plugin in place is not supported — rebuild and reinstall the wheel.
|
||
|
||
### How errors are surfaced
|
||
|
||
There are **three distinct error surfaces**. Knowing which one you are looking at tells you
|
||
what kind of failure occurred.
|
||
|
||
**1. A message box** — a *runtime* failure of an explicit run. You get one when:
|
||
|
||
- your `execute()` **raised an exception** — it is caught at the C++→Python trampoline
|
||
boundary, the full traceback is logged, and the exception is rethrown and shown; or
|
||
- your `execute()` **returned a failure** (`ExecutionResult.failure(...)`, i.e. status
|
||
`RecoverableError` / `FatalError`).
|
||
|
||
For **script** plugins the dialog title is *“Script Plugin Failed”* (or *“Script Plugin”*
|
||
for a returned failure / success), and the body text is the exception message or your
|
||
`ExecutionResult.message`. For **post‑processing** plugins the failure is raised as a
|
||
slicing error (`"Post-processing plugin <name> failed/raised…"`) and surfaces through the
|
||
normal slicing‑error path. Source: `PluginsDialog.cpp` (`run_script_plugin`,
|
||
`complete_with_error`) and `PostProcessor.cpp`.
|
||
|
||
**2. The plugin details / description area** in the Plugins dialog — a *persistent* error
|
||
state stored on the plugin descriptor, not a single run. When a plugin fails to load or has
|
||
invalid metadata, the descriptor records an error (`set_error` / `normalized_error` in
|
||
`PluginDescriptor.hpp`); for a metadata‑invalid plugin **the error text replaces the
|
||
description** shown in the dialog (`PluginsDialog.cpp`). This reflects *state* — “this
|
||
plugin is currently broken” — rather than the result of one execution.
|
||
|
||
**3. The Python log file** — the full traceback. `sys.stderr` is teed to:
|
||
|
||
```
|
||
data_dir()/log/python_<weekday>_<mon>_<day>_<HH>_<MM>_<SS>_<pid>.log
|
||
```
|
||
|
||
(`install_python_stderr_redirect` in `PythonInterpreter.cpp`.) This is the **only** place
|
||
errors from background threads your plugin spawns will appear — those never cross back to
|
||
C++ and never produce a dialog. C++‑side context (load/discovery messages) goes to the main
|
||
session log via Boost.
|
||
|
||
**How to act on each:**
|
||
|
||
- **Message box** → read the message line, then open the `python_*.log` for the file/line
|
||
of the traceback. Dialogs show only the message, not the stack.
|
||
- **Details‑area / Diagnostics error** → the plugin didn’t load; usually a registration
|
||
problem (a capability that doesn’t subclass a typed base, a missing `get_name()`, a
|
||
duplicate capability name, or no `@orca.plugin` package class) or an import error. Fix it,
|
||
then reload.
|
||
- **Anything blocked with a `PermissionError` about a file path** → the audit hook blocked a
|
||
write/read outside the allow‑list. See the *Debugging* section of
|
||
[`plugin_audit_hook.md`](plugin_audit_hook.md) and the `[AUDIT BLOCKED]` log line.
|
||
|
||
**Prefer returning a result over raising** for failures you anticipate:
|
||
`ExecutionResult.failure(orca.PluginResult.RecoverableError, "clear user-facing reason")`
|
||
gives the user a clean message. Raise for genuine bugs — you’ll get a full traceback in the
|
||
log to debug from.
|
||
|
||
### Testing and iterating during development
|
||
|
||
A practical loop:
|
||
|
||
1. **Edit** the plugin source in its `orca_plugins/<plugin>/` folder.
|
||
2. **Reload** — reopen the Plugins dialog / re‑trigger discovery, or restart OrcaSlicer if
|
||
in doubt (instances are captured at load time).
|
||
3. **Run** — for a script plugin use the Plugins dialog **Run** action; for a
|
||
post‑processing plugin run a slice/export so the G‑code pipeline invokes it.
|
||
4. **Watch the log** — keep `data_dir()/log/python_*.log` open (e.g. `tail -f`). Tracebacks,
|
||
`print()` output, and audit blocks all land there.
|
||
5. **Iterate.** Use `ExecutionResult` messages for expected outcomes; rely on the log for
|
||
stack traces.
|
||
|
||
Tips:
|
||
|
||
- Confirm the plugin shows the right name and version in the Plugins dialog, and that **each
|
||
capability you registered** appears (with the expected type) in its expandable capability
|
||
list. A capability that is never passed to `orca.register_capability` will not appear.
|
||
- Develop against small, fast inputs; for post‑processing plugins keep a tiny test model so
|
||
each export cycle is quick.
|
||
- Remember the audit allow‑list: write only under `data_dir()` (or, for G‑code plugins, the
|
||
current G‑code folder). A surprise `PermissionError` is almost always this.
|
||
|
||
---
|
||
|
||
## Part 2 — Adding a New Plugin Type in C++
|
||
|
||
This part is for OrcaSlicer contributors extending the plugin *framework* with a new
|
||
contract — say an “importer” capability type. The system has no per‑type registry/switch for
|
||
*instantiation*: a capability's Python class subclasses a typed base, the package's
|
||
`register_capabilities()` registers it via `register_capability`, and the rest of the app
|
||
reaches the loaded capability instance by `std::dynamic_pointer_cast<ConcreteType>` at the
|
||
call site. So adding a type means: define a base + context + result, add a trampoline that
|
||
forwards into Python (with an audit mode), register pybind11 bindings, wire one call site,
|
||
and add the files to the build.
|
||
|
||
Use the existing `gcode`, `script`, and `printerAgent` types under
|
||
`src/slic3r/plugin/pluginTypes/` as references — `script` is the simplest, `gcode` shows a
|
||
context + scoped audit root, `printerAgent` shows a wide multi‑method interface.
|
||
|
||
### Step 1 — Define the plugin contract (the base class)
|
||
|
||
Create `pluginTypes/<type>/<Type>PluginCapability.hpp`. Subclass `PluginCapabilityInterface`, hardcode
|
||
`get_type()` to your `PluginCapabilityType`, declare your pure‑virtual entry method(s) and any
|
||
context struct, and declare a static `RegisterBindings`. The G‑code base
|
||
(`pluginTypes/gcode/GCodePluginCapability.hpp`) is the canonical small example:
|
||
|
||
```cpp
|
||
#ifndef slic3r_GCodePluginCapability_hpp_
|
||
#define slic3r_GCodePluginCapability_hpp_
|
||
|
||
#include "../../PythonPluginInterface.hpp"
|
||
|
||
namespace Slic3r {
|
||
|
||
struct GCodePluginContext : public PluginContext {
|
||
std::string gcode_path;
|
||
std::string host;
|
||
std::string output_name;
|
||
};
|
||
|
||
class GCodePluginCapability : public PluginCapabilityInterface
|
||
{
|
||
public:
|
||
PluginCapabilityType get_type() const override { return PluginCapabilityType::PostProcessing; }
|
||
|
||
virtual ExecutionResult execute(const GCodePluginContext& ctx) = 0;
|
||
|
||
static void RegisterBindings(pybind11::module_ &module,
|
||
pybind11::enum_<PluginCapabilityType> &pluginTypes);
|
||
};
|
||
|
||
} // namespace Slic3r
|
||
|
||
#endif /* slic3r_GCodePluginCapability_hpp_ */
|
||
```
|
||
|
||
The shared building blocks come from `PythonPluginInterface.hpp`:
|
||
|
||
- `PluginCapabilityInterface` — `virtual std::string get_name() const = 0` (the capability's
|
||
name, provided by Python), `virtual PluginCapabilityType get_type() const` (defaults to
|
||
`Unknown`; typed bases override it), virtual `on_load()` / `on_unload()`, plus the C++‑only
|
||
audit identity (`set_audit_plugin_key`).
|
||
- `struct PluginContext { std::string orca_version; }` — derive your context from this.
|
||
- `struct ExecutionResult { PluginResult status; std::string message, data; }` with static
|
||
`success` / `skipped` / `failure`.
|
||
- `enum class PluginCapabilityType { … }` and the `plugin_capability_type_to_string` /
|
||
`plugin_capability_type_from_string` / `plugin_capability_type_display_name` maps.
|
||
|
||
If your type needs a new `PluginCapabilityType` value, **add it to the enum and to all three
|
||
maps** in `PythonPluginInterface.hpp`, choosing the string the maps translate. Reuse an
|
||
existing value (e.g. `Automation`) if it fits.
|
||
|
||
### Step 2 — Decide the API surface
|
||
|
||
Decide exactly what the plugin must receive and return, and expose **only that**:
|
||
|
||
- **Inputs** go in the context struct (mirror `GCodePluginContext`). Keep it to data the
|
||
plugin legitimately needs.
|
||
- **Outputs** should be an `ExecutionResult` (status + message + `data` string) unless your
|
||
type genuinely needs richer return data — in which case define and bind a small result
|
||
struct.
|
||
- **Keep it minimal and stable.** The bindings are an API surface plugins depend on;
|
||
removing or renaming a bound field/method breaks existing plugins. Add fields rather than
|
||
repurpose them, and prefer the smallest interface that does the job.
|
||
- **Avoid exposing internal slicer/GUI types.** The current API deliberately exposes only
|
||
plain data (strings, enums). Passing raw engine objects to plugins widens both the
|
||
compatibility and the security surface.
|
||
|
||
### Step 3 — Add the trampoline (and choose an audit mode)
|
||
|
||
Create `pluginTypes/<type>/<Type>PluginCapabilityTrampoline.hpp`. Subclass
|
||
`PyPluginCommonTrampoline<YourBase>` (which already provides the `get_name` and
|
||
`on_load`/`on_unload` trampolines) and forward each virtual into Python via
|
||
`ORCA_PY_OVERRIDE_AUDITED`. The G‑code trampoline
|
||
(`pluginTypes/gcode/GCodePluginCapabilityTrampoline.hpp`) in full:
|
||
|
||
```cpp
|
||
#ifndef slic3r_GCodePluginCapabilityTrampoline_hpp_
|
||
#define slic3r_GCodePluginCapabilityTrampoline_hpp_
|
||
|
||
#include <filesystem>
|
||
|
||
#include "../../PyPluginTrampoline.hpp"
|
||
#include "../../PluginAuditManager.hpp"
|
||
#include "GCodePluginCapability.hpp"
|
||
|
||
namespace Slic3r {
|
||
class PyGCodePluginCapabilityTrampoline : public PyPluginCommonTrampoline<GCodePluginCapability>
|
||
{
|
||
public:
|
||
using PyPluginCommonTrampoline<GCodePluginCapability>::PyPluginCommonTrampoline;
|
||
|
||
ExecutionResult execute(const GCodePluginContext& ctx) override
|
||
{
|
||
ORCA_PY_OVERRIDE_AUDITED(
|
||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||
[&] {
|
||
// G-code post-processing plugins may also write into the folder holding the
|
||
// current temp G-code file, in addition to the globally-allowed data_dir().
|
||
// The setup callback runs AFTER the context is constructed so the scoped root
|
||
// is not cleared by ScopedPluginAuditContext's constructor.
|
||
|
||
if (!ctx.gcode_path.empty())
|
||
::Slic3r::PluginAuditManager::instance().add_scoped_allowed_root(
|
||
std::filesystem::path(ctx.gcode_path).parent_path());
|
||
},
|
||
PYBIND11_OVERRIDE_PURE, ExecutionResult, GCodePluginCapability, execute, ctx);
|
||
}
|
||
};
|
||
} // namespace Slic3r
|
||
|
||
#endif
|
||
```
|
||
|
||
The macros (`PyPluginTrampoline.hpp`) do two jobs at this single boundary: log + rethrow the
|
||
Python traceback, and open the filesystem audit scope.
|
||
|
||
```
|
||
ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, /*args...*/)
|
||
```
|
||
|
||
| Argument | Meaning |
|
||
|---|---|
|
||
| `mode` | `AuditMode::Loading` (permissive reads, writes restricted to allow‑list) or `AuditMode::Enforcing` (reads also restricted) — see the audit doc |
|
||
| `audit_setup` | a lambda run *after* the audit context is opened; use it to `add_scoped_allowed_root(...)`. Pass `[] {}` if none |
|
||
| `override_macro` | pybind11’s own `PYBIND11_OVERRIDE` (has a C++ fallback) or `PYBIND11_OVERRIDE_PURE` (pure virtual, no fallback) |
|
||
| `ret, base, name, …` | the standard pybind11 override arguments |
|
||
|
||
> **You must choose an audit mode for every new trampoline method.** Most lifecycle/entry
|
||
> calls use `Loading` (so the plugin can still import modules). Read
|
||
> [`plugin_audit_hook.md`](plugin_audit_hook.md) before picking `Enforcing`.
|
||
|
||
### Step 4 — Register the Python bindings
|
||
|
||
Implement `RegisterBindings` in `pluginTypes/<type>/<Type>PluginCapability.cpp`: create a submodule,
|
||
bind the context/result structs, and bind the base class with its trampoline. The G‑code
|
||
implementation (`pluginTypes/gcode/GCodePluginCapability.cpp`) in full:
|
||
|
||
```cpp
|
||
void GCodePluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
|
||
{
|
||
(void) pluginTypes;
|
||
|
||
auto gcode = module.def_submodule("gcode", "G-code API");
|
||
|
||
py::class_<GCodePluginContext, PluginContext>(gcode, "GCodePluginContext", "Context shared with G-code plugins")
|
||
.def(py::init<>())
|
||
.def_readwrite("gcode_path", &GCodePluginContext::gcode_path)
|
||
.def_readwrite("host", &GCodePluginContext::host)
|
||
.def_readwrite("output_name", &GCodePluginContext::output_name);
|
||
|
||
py::class_<GCodePluginCapability, PluginCapabilityInterface, PyGCodePluginCapabilityTrampoline, std::shared_ptr<GCodePluginCapability>>(gcode, "GCodePluginCapabilityBase")
|
||
.def(py::init<>())
|
||
.def("get_type", &GCodePluginCapability::get_type)
|
||
.def("execute", &GCodePluginCapability::execute);
|
||
}
|
||
```
|
||
|
||
The base class is bound as `GCodePluginCapabilityBase` (the name plugin authors subclass) and
|
||
inherits `get_name` from the root `PythonPluginBase`, so you only bind the type‑specific
|
||
methods here. Then **call your `RegisterBindings` from `bind_python_api`** in
|
||
`PythonPluginBridge.cpp`, next to the existing ones (look for the
|
||
`// Make sure you register your bindings here` comment):
|
||
|
||
```cpp
|
||
// Make sure you register your bindings here
|
||
GCodePluginCapability::RegisterBindings(m, pluginTypes);
|
||
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
|
||
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
|
||
PluginHostApi::RegisterBindings(m);
|
||
// YourTypeCapability::RegisterBindings(m, pluginTypes); // <-- add this
|
||
```
|
||
|
||
The shared `PluginCapabilityType` / `PluginResult` / `PluginContext` / `ExecutionResult` /
|
||
`PythonPluginBase` bindings, the package base (`orca.base`), and the `@orca.plugin` /
|
||
`orca.register_capability` entry points are already defined once in that same function — you
|
||
only add your type‑specific submodule.
|
||
|
||
### Step 5 — Add audit hooks
|
||
|
||
Auditing is not optional. Each trampoline method you wrote in Step 3 already opts into a mode
|
||
through `ORCA_PY_OVERRIDE_AUDITED`. If your type needs a per‑call writable directory (as
|
||
G‑code does for the temp folder), grant it as a **scoped** root in the `audit_setup` lambda;
|
||
prefer scoped roots over widening the global allow‑list. If your type performs a sensitive
|
||
operation the current hook doesn’t yet police, consider extending the hook itself. All of
|
||
this is documented in [`plugin_audit_hook.md`](plugin_audit_hook.md) — read it before
|
||
finalizing the modes.
|
||
|
||
### Step 6 — Hook the type into an OrcaSlicer workflow
|
||
|
||
Nothing runs your plugin until some part of the app invokes it. Pick the invocation pattern
|
||
that matches your type and model it on an existing one:
|
||
|
||
| Type | Where it’s invoked | Pattern |
|
||
|---|---|---|
|
||
| `gcode` | `PostProcessor.cpp` (G‑code export / post‑processing) | resolve the preset's capability refs, `dynamic_pointer_cast<GCodePluginCapability>(cap->instance)`, build `GCodePluginContext`, call `execute(ctx)` under the GIL |
|
||
| `script` | `PluginsDialog.cpp` (Run action) | `get_plugin_capability_by_name(...)`, `dynamic_pointer_cast<ScriptPluginCapability>(cap->instance)`, call `execute()` |
|
||
| `printerAgent` | `NetworkAgentFactory.cpp`, wired in `GUI_App.cpp` | register via `subscribe_on_capability_load_callback` / `subscribe_on_capability_unload_callback`; the callback filters by `capability.type == PluginCapabilityType::PrinterConnection`, then registers/deregisters an agent |
|
||
|
||
For your new type, add a call site (or an on‑capability‑load callback) that:
|
||
|
||
1. obtains a loaded capability (via `PluginLoader::get_plugin_capabilities_by_type(...)` or
|
||
`get_plugin_capability_by_name(...)`) and does
|
||
`std::dynamic_pointer_cast<YourTypeCapability>(cap->instance)`;
|
||
2. on a successful cast, builds the context and invokes your entry method under the GIL;
|
||
3. if your type needs unload cleanup, add a case to the capability‑teardown switch (keyed on
|
||
`PluginCapabilityType`) in `PluginLoader.cpp`.
|
||
|
||
> **Disabled / missing plugins must not change existing behavior.** Every existing path is
|
||
> gated on a successful `dynamic_pointer_cast` (or a `type ==` check) and iterates only over
|
||
> installed/selected plugins, so when none of your type is installed the loop or callback
|
||
> simply finds nothing and does nothing. Follow the same pattern — never run unconditional
|
||
> work on behalf of a plugin type that isn’t present.
|
||
|
||
### Step 7 — Add the files to the build
|
||
|
||
List your new `.hpp` / `.cpp` files in `src/slic3r/CMakeLists.txt`, alongside the existing
|
||
plugin‑type sources (search for `plugin/pluginTypes/gcode/GCodePluginCapability.cpp` — the block is
|
||
around lines 615–623):
|
||
|
||
```cmake
|
||
plugin/pluginTypes/<type>/<Type>PluginCapability.hpp
|
||
plugin/pluginTypes/<type>/<Type>PluginCapability.cpp
|
||
plugin/pluginTypes/<type>/<Type>PluginCapabilityTrampoline.hpp
|
||
```
|
||
|
||
### Recipe at a glance
|
||
|
||
1. **Enum/maps** (if new type): add a `PluginCapabilityType` value + the three string maps in
|
||
`PythonPluginInterface.hpp`.
|
||
2. **Contract**: `pluginTypes/<type>/<Type>PluginCapability.hpp` — base + context + result + static
|
||
`RegisterBindings`.
|
||
3. **Trampoline**: `pluginTypes/<type>/<Type>PluginCapabilityTrampoline.hpp` — forward each virtual via
|
||
`ORCA_PY_OVERRIDE_AUDITED`, choosing an audit mode.
|
||
4. **Bindings**: `pluginTypes/<type>/<Type>PluginCapability.cpp` `RegisterBindings`, then call it from
|
||
`bind_python_api` in `PythonPluginBridge.cpp`.
|
||
5. **Audit**: confirm the modes / scoped roots per `plugin_audit_hook.md`.
|
||
6. **Workflow**: add a call site / on‑load callback that casts and invokes; gate it so an
|
||
absent type is a no‑op.
|
||
7. **Build**: add the files to `src/slic3r/CMakeLists.txt`.
|
||
|
||
---
|
||
|
||
## Part 3 — Testing and Verification
|
||
|
||
There is **no dedicated automated test suite for the Python plugin system today.**
|
||
Verification is primarily manual, with targeted Catch2 tests where the logic is pure C++.
|
||
|
||
### Manual testing
|
||
|
||
- **Loading** — install/side‑load a plugin into `data_dir()/orca_plugins/<name>/`, open the
|
||
Plugins dialog, and confirm it appears with the correct name and version and that each
|
||
registered capability is listed (with the expected type). A plugin that fails to load shows
|
||
its error in the Diagnostics tab (see
|
||
[How errors are surfaced](#how-errors-are-surfaced)).
|
||
- **Execution** — script plugins: use the dialog **Run** action. Post‑processing plugins:
|
||
run a slice/export and confirm the plugin ran (e.g. its effect on the G‑code, plus log
|
||
output). Printer‑agent plugins: verify the agent registers on load and deregisters on
|
||
unload.
|
||
- **Error handling** — deliberately make the plugin (a) raise an exception and (b) return
|
||
`ExecutionResult.failure(...)`; confirm the message box text, and that the full traceback
|
||
appears in `data_dir()/log/python_*.log`. Confirm an invalid‑metadata plugin surfaces its
|
||
error in the details area rather than crashing.
|
||
- **Audit** — confirm a write outside the allow‑list is blocked with a `PermissionError` and
|
||
an `[AUDIT BLOCKED]` log line, and that legitimate writes (under `data_dir()`, or the
|
||
G‑code folder for G‑code plugins) succeed.
|
||
|
||
### Automated tests where appropriate
|
||
|
||
Add **targeted Catch2 tests** (under `tests/`) for the pure‑C++ pieces that don’t need a
|
||
running interpreter or GUI — for example:
|
||
|
||
- PEP 723 metadata parsing (`parse_pep723_toml` / `read_python_plugin_metadata` in
|
||
`PythonFileUtils.cpp`): valid blocks, missing fields, malformed arrays.
|
||
- Capability reference parsing/serialization (`parse_capability_ref` in `Config.cpp`) — see
|
||
`tests/libslic3r/test_config.cpp` and `tests/slic3rutils/test_plugin_capability_identifier.cpp`
|
||
for local vs. cloud refs and malformed input.
|
||
- The audit allow‑list logic (`PluginAuditManager::check_open`, `is_inside_allowed_root`):
|
||
inside/outside roots, `..` traversal, read vs write under each mode.
|
||
- Type‑string round‑trips (`plugin_capability_type_from_string` / `plugin_capability_type_to_string`).
|
||
|
||
Anything that requires the embedded interpreter, file installs, or GUI dialogs is currently
|
||
best covered by the manual steps above.
|
||
|
||
### Cross‑platform and regression checks
|
||
|
||
- **Cross‑platform** — the plugin code must build and run on Windows, macOS, and Linux. Be
|
||
careful with path handling (the audit allow‑list canonicalizes paths; keep using
|
||
`std::filesystem` / the existing helpers), and with line endings in the PEP 723 parser
|
||
(it already strips `\r`).
|
||
- **No regressions** — changes to the framework must not alter behavior when no plugin of a
|
||
given type is installed (Step 6). When touching the trampoline/audit headers, note that
|
||
`PyPluginTrampoline.hpp` and `PluginAuditManager.hpp` are included by many translation
|
||
units; a header‑only change may need a clean rebuild of the affected targets to take
|
||
effect (see the audit doc’s *Debugging* section).
|
||
- **Backward compatibility** — don’t rename or remove bound fields/methods or
|
||
`PluginCapabilityType` values that existing plugins or installed profiles may depend on; add
|
||
rather than repurpose.
|
||
|
||
---
|
||
|
||
## Key files
|
||
|
||
| File | Responsibility |
|
||
|---|---|
|
||
| `src/slic3r/plugin/PythonPluginInterface.hpp` | `PluginCapabilityType`, `PluginContext`, `PluginResult`, `ExecutionResult`, `PluginCapabilityInterface`, type‑string maps |
|
||
| `src/slic3r/plugin/PythonPluginBridge.{hpp,cpp}` | the `orca` module (`bind_python_api`), `@orca.plugin` / `register_capability`, package + capability capture/instantiation |
|
||
| `src/slic3r/plugin/PyPluginPackage.hpp` | the package base (`orca.base`) and its `register_capabilities` |
|
||
| `src/slic3r/plugin/PyPluginTrampoline.hpp` | the `ORCA_PY_*` trampoline macros (traceback logging + audit scope) and common trampolines |
|
||
| `src/slic3r/plugin/pluginTypes/<type>/` | per‑type capability base (`*PluginCapability.hpp/.cpp`) and trampoline (`*PluginCapabilityTrampoline.hpp`) |
|
||
| `src/slic3r/plugin/PluginDescriptor.hpp` | per‑plugin metadata + error state (`set_error`, `normalized_error`, `is_metadata_valid`) |
|
||
| `src/slic3r/plugin/PythonFileUtils.cpp` | PEP 723 / wheel metadata parsing, entry‑file discovery |
|
||
| `src/slic3r/plugin/PluginCatalog.cpp`, `PluginLoader.cpp` | discovery, install, load lifecycle, dependency install |
|
||
| `src/slic3r/plugin/PythonInterpreter.cpp` | interpreter init, audit‑hook install, traceback formatting, `stderr` → log file |
|
||
| `src/slic3r/GUI/PluginsDialog.cpp` | Plugins dialog: details/error area, script **Run**, error dialogs |
|
||
| `src/slic3r/GUI/PostProcessor.cpp` | resolves the preset's plugin refs and invokes post‑processing (G‑code) capabilities during export |
|
||
| `src/slic3r/CMakeLists.txt` (~609–623) | build list for plugin sources |
|
||
| [`plugin_audit_hook.md`](plugin_audit_hook.md) | the audit hook: modes, allow‑list, extending it |
|