feat(plugin): expose the slicing print-graph as raw orca.host classes + Twistify sample

Adds PluginHostSlicing, which registers the print-graph data model (Print,
PrintObject, Layer, LayerRegion, Surface, ExPolygon, extrusions, ...) into the
orca.host submodule in the same raw-class style as PluginHostApi's Model/Preset
graph, with shared helpers in PluginBindingUtils. SlicingPipelinePluginCapability
is trimmed to the capability surface (the standalone SlicingNumpy helper is folded
away). Adds the Twistify example plugin next to Inset and broadens the binding,
hook, and plugin-install tests.
This commit is contained in:
SoftFever
2026-07-08 00:05:28 +08:00
parent aafcccc83c
commit f81a24abfb
29 changed files with 1718 additions and 962 deletions

View File

@@ -1,120 +0,0 @@
# /// 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)