mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-16 07:22:10 +00:00
feat(plugin): add the slicing-pipeline plugin capability
Introduces a plugin capability that runs Python at the seams of Print::process(), letting a plugin read and rewrite slicing state as it is computed. - New slicing_pipeline_plugin config option; selected plugin refs are serialized into the print manifest. - Print gains an injectable hook fired at each pipeline step (posSlice, posPerimeters, posInfill, ...). It is a no-op when unset, fires only on genuine (re)computation, and never on the use-cache path. - orca.slicing submodule: SlicingPipelineCapabilityBase plus a trampoline and a Step enum. Capabilities read the live graph through zero-copy int64 numpy views (contour/holes geometry with unscaled coordinates, flattened toolpath data) and edit it through 2D-geometry mutators with cache-invariant refresh. - GUI dispatcher runs capabilities during slicing under the GIL, turns plugin errors into slicing errors, honors cancellation, and adds the plugin picker. - Ships the InsetEverySlice sample plugin and binding/hook tests.
This commit is contained in:
143
docs/plugins/slicing_pipeline_plugin.md
Normal file
143
docs/plugins/slicing_pipeline_plugin.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Slicing Pipeline Plugins
|
||||
|
||||
> This note is a companion to the general Python plugin documentation (see the
|
||||
> OrcaSlicer wiki for `plugin_development.md` / `plugin_system.md` /
|
||||
> `plugin_audit_hook.md` — the plugin-doc set was migrated there and no longer
|
||||
> lives under `docs/` in this repository). It covers only what is specific to
|
||||
> the `SlicingPipeline` capability: `orca.slicing.SlicingPipelineCapabilityBase`.
|
||||
> Read it alongside the worked sample at
|
||||
> [`resources/orca_plugins/InsetEverySlice.py`](../../resources/orca_plugins/InsetEverySlice.py).
|
||||
|
||||
A `SlicingPipeline` capability is invoked by OrcaSlicer at several seams inside
|
||||
`Print::process()`, on the slicing worker thread, so it can read — and in one case,
|
||||
mutate — the intermediate data the slicer produces between the raw mesh and the
|
||||
final G-code. It is research/experimental: the read graph is broad, but only one
|
||||
mutation is fully wired through to the toolpath output today.
|
||||
|
||||
```python
|
||||
class MyCapability(orca.slicing.SlicingPipelineCapabilityBase):
|
||||
def get_name(self):
|
||||
return "My Capability"
|
||||
def execute(self, ctx: orca.slicing.SlicingPipelineContext):
|
||||
...
|
||||
return orca.ExecutionResult.success()
|
||||
```
|
||||
|
||||
## When `execute()` fires, and what `ctx.object` is
|
||||
|
||||
`ctx.step` is one of the `orca.slicing.Step` values, in the order they occur inside
|
||||
one `Print::process()` run: `Slice`, `Perimeters`, `EstimateCurledExtrusions`,
|
||||
`Infill`, `Ironing`, `Contouring`, `SupportMaterial`, `DetectOverhangsForLift`,
|
||||
`WipeTower`, `SkirtBrim`, `SimplifyPath`. Note that `SimplifyPath` is declared
|
||||
before `WipeTower` and `SkirtBrim` in the `Step` enum, but fires after them at runtime.
|
||||
|
||||
Most steps are **per-object**: `execute()` runs once per `PrintObject` that just
|
||||
(re)computed that step, and `ctx.object` is a `PrintObjectView` for it. `WipeTower`
|
||||
and `SkirtBrim` are **print-wide**: they run once per slice, and `ctx.object` is
|
||||
`None`. Always check both `ctx.step` and `ctx.object` before touching object data —
|
||||
see `InsetEverySlice.execute()` for the standard guard:
|
||||
|
||||
```python
|
||||
if ctx.step != orca.slicing.Step.Slice or ctx.object is None:
|
||||
return orca.ExecutionResult.success()
|
||||
```
|
||||
|
||||
The hook fires **only on genuine recomputation** of that step for that object — an
|
||||
incremental re-slice that finds a step already cached does not re-invoke `execute()`
|
||||
for it (see "Persistence and duplicates" below).
|
||||
|
||||
## Supported mutations, per step
|
||||
|
||||
The read graph (`PrintObjectView` → `LayerView` → `LayerRegionView` →
|
||||
`SurfaceView`/`PathData`) is available at every step. Mutation is narrower:
|
||||
|
||||
| Mutator | Step it makes sense at | Cascade |
|
||||
|---|---|---|
|
||||
| `LayerRegionView.set_slices(polygons)` | `Step.Slice` | **Fully supported.** The split slice loop calls `make_perimeters()` immediately after the `Slice` hook, so the new geometry flows into perimeters, infill and the final G-code — the toolpath preview visibly changes. This is the primary, recommended mutation entry point. |
|
||||
| `LayerRegionView.set_fill_surfaces(polygons)` | `Step.Infill` | **Limited.** Replaces the stored fill-prep surfaces but does **not** regenerate the `fills` toolpaths already built for that region in v1 — the surface data changes, the rendered infill does not (yet). |
|
||||
| `LayerView.set_lslices(islands)` | any step where a `LayerView` is reachable | **Limited / read-oriented.** Replaces the layer's merged islands and refreshes the `lslices_bboxes` cache so that invariant stays consistent, but no further cascade is documented — treat it as advanced/diagnostic, not a way to redirect downstream computation. |
|
||||
| `SurfaceView.set_type(surface_type)` | any step where a `SurfaceView` is reachable | **Limited.** Reassigns `surface_type` only; the geometry is untouched, and nothing downstream is automatically regenerated as a result. |
|
||||
|
||||
Every other step (`Perimeters`, `EstimateCurledExtrusions`, `Ironing`, `Contouring`,
|
||||
`SupportMaterial`, `DetectOverhangsForLift`, `SimplifyPath`, `WipeTower`,
|
||||
`SkirtBrim`) exposes **read-only** access in practice: the views are there, but
|
||||
nothing calls back into a not-yet-run earlier step, so writes there have no
|
||||
guaranteed effect on the final output. Treat non-`Slice` steps as inspection
|
||||
points, and do real geometry edits through `set_slices()` at `Step.Slice`.
|
||||
|
||||
**Gotcha:** `set_slices()`/`set_fill_surfaces()` build every replacement `Surface`
|
||||
from the *first* surface in the collection being replaced (or `stInternal` if the
|
||||
region had none) — per-surface `surface_type` distinctions among the surfaces you
|
||||
pass in are **not** preserved individually. If a region's slices mix top/bottom/
|
||||
internal surfaces and you need to keep that distinction, mutate contours, then
|
||||
restore per-surface types with `SurfaceView.set_type()` afterward.
|
||||
|
||||
## Scaled coordinates are `int64`, and the scale is live
|
||||
|
||||
Every point (`ExPolygonView.contour()`/`holes()`, `PathData.points()`) is a
|
||||
read-only `int64` NumPy array of internal scaled units, not millimeters. Convert
|
||||
with `orca.slicing.unscale(coord)` — **never** hardcode `1e-6`/`1e6`. The scale
|
||||
factor is not a fixed constant in this codebase (larger beds use a coarser scale),
|
||||
so it must be read at call time:
|
||||
|
||||
```python
|
||||
mm_per_unit = orca.slicing.unscale(1) # read the live scale
|
||||
one_mm_scaled = int(round(1.0 / mm_per_unit)) # -> scaled-unit equivalent of 1mm
|
||||
```
|
||||
|
||||
`InsetEverySlice` follows exactly this pattern for its 1mm inset.
|
||||
|
||||
## Lifetime: every view and array is valid only during `execute(ctx)`
|
||||
|
||||
`PrintObjectView`, `LayerView`, `LayerRegionView`, `SurfaceView`, `ExPolygonView`,
|
||||
and `PathData` are thin, non-owning wrappers over memory owned by the `Print`
|
||||
being sliced. The NumPy arrays they hand out are zero-copy: they alias that same
|
||||
memory. All of it is valid **only for the duration of the `execute(ctx)` call that
|
||||
produced it** — the underlying `std::vector` storage can be reallocated by the very
|
||||
next pipeline step. Do not stash a view, a `SurfaceView`, or an array in `self.*`
|
||||
and read it from a later `execute()` call, and do not return one from `execute()`.
|
||||
Read what you need, copy any plain Python values out (`int()`, `.tolist()`, etc. —
|
||||
never the array itself) if you must keep them, and let the rest go when the call
|
||||
returns.
|
||||
|
||||
## Persistence and duplicates
|
||||
|
||||
A `set_slices()` mutation is written directly into the `PrintObject`'s `Layer`
|
||||
data, not into some separate plugin-owned overlay:
|
||||
|
||||
- **It survives across steps within the same slice** — that's what makes the
|
||||
cascade into perimeters/infill/G-code work.
|
||||
- **It survives an incremental re-slice only while `posSlice` stays cached *and*
|
||||
perimeters are not re-run (v1 limitation).** `slice()` backs up the *pre-hook*
|
||||
geometry into each layer's `raw_slices` before the `Slice` hook fires, and
|
||||
`make_perimeters()` calls `restore_untyped_slices()`, which overwrites
|
||||
`slices` from that backup. So a config change that only invalidates a *later*
|
||||
step but still re-runs perimeters (e.g. `wall_loops`) silently reverts the
|
||||
mutation to the original geometry, while `posSlice` stays cached so the `Slice`
|
||||
hook does **not** fire again to re-apply it. Propagating the mutation into
|
||||
`raw_slices` so it survives a perimeter re-run is a known v1 limitation; for
|
||||
now, force a genuine re-slice (see below) if you need the mutation reapplied.
|
||||
- **Toggling which plugins are selected always gets a clean slice.** Changing the
|
||||
`Slicing Pipeline Plugin` picker selection itself invalidates `posSlice`, so
|
||||
selecting or deselecting a plugin forces a genuine re-slice (and re-fires the
|
||||
hook, or stops firing it) rather than leaving stale mutated geometry behind.
|
||||
- **Duplicated (identical) objects share the same `Layer*`.** Mutating the
|
||||
instance that actually slices is automatically visible on every duplicate of
|
||||
it. An object that must diverge from its duplicates cannot be an exact
|
||||
duplicate of them.
|
||||
|
||||
## Errors, `FatalError`, and cancellation
|
||||
|
||||
`execute()` runs under the GIL, inside a `try`/`catch` on the host side. Any
|
||||
uncaught Python exception, or returning
|
||||
`orca.ExecutionResult.failure(orca.PluginResult.FatalError, message)`, is converted
|
||||
into a `Slic3r::SlicingError` tagged with the plugin's capability name and your
|
||||
message. That surfaces to the user as a normal (non-fatal) slicing-error
|
||||
notification — it aborts that slice, but it does not crash the app. Prefer this
|
||||
over letting exceptions propagate silently, and put anything you need the user to
|
||||
see in the message.
|
||||
|
||||
Check `ctx.cancelled()` if you are doing meaningfully expensive work in a loop
|
||||
(e.g. a large multi-object print) so a user-initiated cancel is honored promptly
|
||||
instead of only at the next step boundary; `InsetEverySlice` demonstrates the
|
||||
check on its per-layer loop even though its own work is cheap.
|
||||
Reference in New Issue
Block a user