mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-16 07:22:10 +00:00
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.
121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
# /// script
|
|
# requires-python = ">=3.12"
|
|
# dependencies = ["numpy"]
|
|
#
|
|
# [tool.orcaslicer.plugin]
|
|
# name = "Inset Every Slice"
|
|
# description = "Insets every layer's slices by 1mm at the Slice boundary (demo)."
|
|
# author = "OrcaSlicer"
|
|
# version = "1.0.0"
|
|
# type = "slicing-pipeline"
|
|
# ///
|
|
"""Inset Every Slice -- a small, WORKING SlicingPipeline sample plugin.
|
|
|
|
At Step.Slice, for every layer/region of the sliced object, this shrinks each
|
|
sliced surface's outer contour by INSET_MM and writes the result back with
|
|
LayerRegionView.set_slices(). set_slices() at Step.Slice is the fully-supported
|
|
mutation-cascade entry point (see docs/plugins/slicing_pipeline_plugin.md next
|
|
to this file): the split slice loop runs make_perimeters() right after the
|
|
Slice hook, so the change cascades into perimeters, infill and the final
|
|
G-code -- the toolpath preview visibly shrinks.
|
|
|
|
This is a *teaching* sample, not a production-grade offset:
|
|
- The inset is a per-axis contraction toward the contour's bounding-box
|
|
center: each vertex coordinate is pulled toward the center by up to
|
|
INSET_MM, independently on X and Y, and never crosses the center. That is
|
|
an exact inward offset for a convex, axis-aligned contour (e.g. the square
|
|
cross-section of a plain cube, which is what the manual test in the design
|
|
docs uses) but it is NOT a general polygon offset -- it will distort a
|
|
rotated or non-rectangular contour. A real plugin should reach for a proper
|
|
offset library (e.g. Shapely's buffer(), or Clipper) instead.
|
|
- Holes are passed through unchanged. A correct hole inset needs an
|
|
*outward* offset plus re-validating containment against the shrunk outer
|
|
contour, which is more than a short demo should attempt.
|
|
- Degenerate contours (fewer than 3 points, or a shape too small for a 1mm
|
|
inset without inverting) are left unmodified rather than mutated into
|
|
garbage.
|
|
|
|
numpy is declared as a dependency: the read views hand back zero-copy int64
|
|
ndarrays, and set_slices() requires genuine ndarrays back (not plain lists),
|
|
so building the modified contour needs numpy.
|
|
"""
|
|
import numpy as np
|
|
import orca
|
|
|
|
INSET_MM = 1.0
|
|
|
|
|
|
def _pull(value, center, amount):
|
|
"""Move `value` toward `center` by up to `amount`, never crossing it."""
|
|
if value > center:
|
|
return max(center, value - amount)
|
|
if value < center:
|
|
return min(center, value + amount)
|
|
return center
|
|
|
|
|
|
def _inset_contour(contour, inset_scaled):
|
|
"""Axis-aligned inward contraction of an (N,2) int64 contour.
|
|
|
|
Returns a new (N,2) int64 array, or None if the contour is degenerate
|
|
(fewer than 3 points) or too small for `inset_scaled` without inverting.
|
|
"""
|
|
if contour.shape[0] < 3:
|
|
return None
|
|
xs, ys = contour[:, 0], contour[:, 1]
|
|
min_x, max_x = int(xs.min()), int(xs.max())
|
|
min_y, max_y = int(ys.min()), int(ys.max())
|
|
if (max_x - min_x) <= 2 * inset_scaled or (max_y - min_y) <= 2 * inset_scaled:
|
|
return None # shape too small on at least one axis: inset would invert it
|
|
cx, cy = (min_x + max_x) // 2, (min_y + max_y) // 2
|
|
|
|
out = contour.copy()
|
|
for i in range(contour.shape[0]):
|
|
out[i, 0] = _pull(int(contour[i, 0]), cx, inset_scaled)
|
|
out[i, 1] = _pull(int(contour[i, 1]), cy, inset_scaled)
|
|
return out
|
|
|
|
|
|
class InsetEverySlice(orca.slicing.SlicingPipelineCapabilityBase):
|
|
def get_name(self):
|
|
return "Inset Every Slice"
|
|
|
|
def execute(self, ctx):
|
|
if ctx.step != orca.slicing.Step.Slice or ctx.object is None:
|
|
return orca.ExecutionResult.success()
|
|
|
|
# Millimeters -> scaled integer units via the *live* scale. SCALING_FACTOR
|
|
# is not a fixed constant (large beds use a coarser scale), so this must be
|
|
# read at call time -- never hardcode 1e6/1e-6.
|
|
inset_scaled = int(round(INSET_MM / orca.slicing.unscale(1)))
|
|
|
|
regions_touched = 0
|
|
for layer in ctx.object.layers():
|
|
if ctx.cancelled():
|
|
break
|
|
for region in layer.regions():
|
|
surfaces = region.slices()
|
|
if not surfaces:
|
|
continue # set_slices() rejects an empty list
|
|
|
|
new_surfaces = []
|
|
for surface in surfaces:
|
|
expoly = surface.expolygon
|
|
contour = expoly.contour()
|
|
inset = _inset_contour(contour, inset_scaled)
|
|
if inset is not None:
|
|
contour = inset
|
|
# Holes are passed through unchanged -- see module docstring.
|
|
new_surfaces.append([contour, expoly.holes()])
|
|
|
|
region.set_slices(new_surfaces)
|
|
regions_touched += 1
|
|
|
|
return orca.ExecutionResult.success(f"inset applied to {regions_touched} region(s)")
|
|
|
|
|
|
@orca.plugin
|
|
class InsetEverySlicePackage(orca.base):
|
|
def register_capabilities(self):
|
|
orca.register_capability(InsetEverySlice)
|